1
mirror of https://github.com/flipperdevices/flipperzero-firmware.git synced 2025-12-12 04:41:26 +04:00

[FL-3925] JS views finished (#4155)

* js: value destructuring and tests

* js: temporary fix to see size impact

* js_val: reduce code size 1

* i may be stupid.

* test: js_value args

* Revert "js: temporary fix to see size impact"

This reverts commit f51d726dba.

* pvs: silence warnings

* style: formatting

* pvs: silence warnings?

* pvs: silence warnings??

* js_value: redesign declaration types for less code

* js: temporary fix to see size impact

* style: formatting

* pvs: fix helpful warnings

* js_value: reduce .rodata size

* pvs: fix helpful warning

* js_value: reduce code size 1

* fix build error

* style: format

* Revert "js: temporary fix to see size impact"

This reverts commit d6a46f0179.

* style: format

* js: move to new arg parser

* style: format

* feat: all js views done

* js, toolbox: generalize string owning

* toolbox: silence pvs warning

---------

Co-authored-by: hedger <hedger@nanode.su>
Co-authored-by: hedger <hedger@users.noreply.github.com>
This commit is contained in:
Anna Antonenko
2025-09-24 23:24:28 +04:00
committed by GitHub
parent 0d5beedb01
commit d0360625d6
30 changed files with 1496 additions and 48 deletions

View File

@@ -93,13 +93,12 @@ static bool popup_view_input_callback(InputEvent* event, void* context) {
void popup_start_timer(void* context) {
Popup* popup = context;
if(popup->timer_enabled) {
uint32_t timer_period =
popup->timer_period_in_ms / (1000.0f / furi_kernel_get_tick_frequency());
uint32_t timer_period = furi_ms_to_ticks(popup->timer_period_in_ms);
if(timer_period == 0) timer_period = 1;
if(furi_timer_start(popup->timer, timer_period) != FuriStatusOk) {
furi_crash();
};
}
}
}

View File

@@ -41,7 +41,7 @@ void popup_free(Popup* popup);
*/
View* popup_get_view(Popup* popup);
/** Set popup header text
/** Set popup timeout callback
*
* @param popup Popup instance
* @param callback PopupCallback

View File

@@ -78,6 +78,54 @@ App(
sources=["modules/js_gui/text_input.c"],
)
App(
appid="js_gui__number_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_number_input_ep",
requires=["js_app"],
sources=["modules/js_gui/number_input.c"],
)
App(
appid="js_gui__button_panel",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_button_panel_ep",
requires=["js_app"],
sources=["modules/js_gui/button_panel.c"],
)
App(
appid="js_gui__popup",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_popup_ep",
requires=["js_app"],
sources=["modules/js_gui/popup.c"],
)
App(
appid="js_gui__button_menu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_button_menu_ep",
requires=["js_app"],
sources=["modules/js_gui/button_menu.c"],
)
App(
appid="js_gui__menu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_menu_ep",
requires=["js_app"],
sources=["modules/js_gui/menu.c"],
)
App(
appid="js_gui__vi_list",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_vi_list_ep",
requires=["js_app"],
sources=["modules/js_gui/vi_list.c"],
)
App(
appid="js_gui__byte_input",
apptype=FlipperAppType.PLUGIN,

View File

@@ -9,6 +9,12 @@ let byteInputView = require("gui/byte_input");
let textBoxView = require("gui/text_box");
let dialogView = require("gui/dialog");
let filePicker = require("gui/file_picker");
let buttonMenuView = require("gui/button_menu");
let buttonPanelView = require("gui/button_panel");
let menuView = require("gui/menu");
let numberInputView = require("gui/number_input");
let popupView = require("gui/popup");
let viListView = require("gui/vi_list");
let widget = require("gui/widget");
let icon = require("gui/icon");
let flipper = require("flipper");
@@ -27,6 +33,11 @@ let stopwatchWidgetElements = [
{ element: "button", button: "right", text: "Back" },
];
// icons for the button panel
let offIcons = [icon.getBuiltin("off_19x20"), icon.getBuiltin("off_hover_19x20")];
let powerIcons = [icon.getBuiltin("power_19x20"), icon.getBuiltin("power_hover_19x20")];
let settingsIcon = icon.getBuiltin("Settings_14");
// declare view instances
let views = {
loading: loadingView.make(),
@@ -48,19 +59,57 @@ let views = {
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
}),
stopwatchWidget: widget.makeWith({}, stopwatchWidgetElements),
buttonMenu: buttonMenuView.makeWith({
header: "Header"
}, [
{ type: "common", label: "Test" },
{ type: "control", label: "Test2" },
]),
buttonPanel: buttonPanelView.makeWith({
matrixSizeX: 2,
matrixSizeY: 2,
}, [
{ type: "button", x: 0, y: 0, matrixX: 0, matrixY: 0, icon: offIcons[0], iconSelected: offIcons[1] },
{ type: "button", x: 30, y: 30, matrixX: 1, matrixY: 1, icon: powerIcons[0], iconSelected: powerIcons[1] },
{ type: "label", x: 0, y: 50, text: "Label", font: "primary" },
]),
menu: menuView.makeWith({}, [
{ label: "One", icon: settingsIcon },
{ label: "Two", icon: settingsIcon },
{ label: "three", icon: settingsIcon },
]),
numberKbd: numberInputView.makeWith({
header: "Number input",
defaultValue: 100,
minValue: 0,
maxValue: 200,
}),
popup: popupView.makeWith({
header: "Hello",
text: "I'm going to be gone\nin 2 seconds",
}),
viList: viListView.makeWith({}, [
{ label: "One", variants: ["1", "1.0"] },
{ label: "Two", variants: ["2", "2.0"] },
]),
demos: submenuView.makeWith({
header: "Choose a demo",
items: [
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Widget",
"Exit app",
],
}),
}, [
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Widget",
"Button menu",
"Button panel",
"Menu",
"Number input",
"Popup",
"Var. item list",
"Exit app",
]),
};
// demo selector
@@ -92,6 +141,19 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v
} else if (index === 6) {
gui.viewDispatcher.switchTo(views.stopwatchWidget);
} else if (index === 7) {
gui.viewDispatcher.switchTo(views.buttonMenu);
} else if (index === 8) {
gui.viewDispatcher.switchTo(views.buttonPanel);
} else if (index === 9) {
gui.viewDispatcher.switchTo(views.menu);
} else if (index === 10) {
gui.viewDispatcher.switchTo(views.numberKbd);
} else if (index === 11) {
views.popup.set("timeout", 2000);
gui.viewDispatcher.switchTo(views.popup);
} else if (index === 12) {
gui.viewDispatcher.switchTo(views.viList);
} else if (index === 13) {
eventLoop.stop();
}
}, gui, eventLoop, views);
@@ -156,6 +218,42 @@ eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, vie
return [views, stopwatchWidgetElements, halfSeconds];
}, views, stopwatchWidgetElements, 0);
// go back after popup times out
eventLoop.subscribe(views.popup.timeout, function (_sub, _item, gui, views) {
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// button menu callback
eventLoop.subscribe(views.buttonMenu.input, function (_sub, input, gui, views) {
views.helloDialog.set("text", "You selected #" + input.index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// button panel callback
eventLoop.subscribe(views.buttonPanel.input, function (_sub, input, gui, views) {
views.helloDialog.set("text", "You selected #" + input.index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// menu callback
eventLoop.subscribe(views.menu.chosen, function (_sub, index, gui, views) {
views.helloDialog.set("text", "You selected #" + index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// menu callback
eventLoop.subscribe(views.numberKbd.input, function (_sub, number, gui, views) {
views.helloDialog.set("text", "You typed " + number.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// ignore VI list
eventLoop.subscribe(views.viList.valueUpdate, function (_sub, _item) {});
// run UI
gui.viewDispatcher.switchTo(views.demos);
eventLoop.run();

View File

@@ -0,0 +1,169 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/button_menu.h>
#include <toolbox/str_buffer.h>
typedef struct {
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsBtnMenuContext;
typedef struct {
int32_t index;
InputType input_type;
} JsBtnMenuEvent;
static const char* js_input_type_to_str(InputType type) {
switch(type) {
case InputTypePress:
return "press";
case InputTypeRelease:
return "release";
case InputTypeShort:
return "short";
case InputTypeLong:
return "long";
case InputTypeRepeat:
return "repeat";
default:
furi_crash();
}
}
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnMenuContext* context) {
UNUSED(context);
JsBtnMenuEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("index", mjs_mk_number(mjs, event.index));
JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false));
}
return event_obj;
}
static void input_callback(void* ctx, int32_t index, InputType type) {
JsBtnMenuContext* context = ctx;
JsBtnMenuEvent event = {
.index = index,
.input_type = type,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool matrix_header_assign(
struct mjs* mjs,
ButtonMenu* menu,
JsViewPropValue value,
JsBtnMenuContext* context) {
UNUSED(mjs);
button_menu_set_header(menu, str_buffer_make_owned_clone(&context->str_buffer, value.string));
return true;
}
static bool js_button_menu_add_child(
struct mjs* mjs,
ButtonMenu* menu,
JsBtnMenuContext* context,
mjs_val_t child_obj) {
static const JsValueEnumVariant js_button_menu_item_type_variants[] = {
{"common", ButtonMenuItemTypeCommon},
{"control", ButtonMenuItemTypeControl},
};
static const JsValueDeclaration js_button_menu_item_type =
JS_VALUE_ENUM(ButtonMenuItemType, js_button_menu_item_type_variants);
static const JsValueDeclaration js_button_menu_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField js_button_menu_child_fields[] = {
{"type", &js_button_menu_item_type},
{"label", &js_button_menu_string},
};
static const JsValueDeclaration js_button_menu_child =
JS_VALUE_OBJECT(js_button_menu_child_fields);
ButtonMenuItemType item_type;
const char* label;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_menu_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&item_type,
&label);
if(status != JsValueParseStatusOk) return false;
button_menu_add_item(
menu,
str_buffer_make_owned_clone(&context->str_buffer, label),
context->next_index++,
input_callback,
item_type,
context);
return true;
}
static void js_button_menu_reset_children(ButtonMenu* menu, JsBtnMenuContext* context) {
context->next_index = 0;
button_menu_reset(menu);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsBtnMenuContext* ctx_make(struct mjs* mjs, ButtonMenu* menu, mjs_val_t view_obj) {
UNUSED(menu);
JsBtnMenuContext* context = malloc(sizeof(JsBtnMenuContext));
*context = (JsBtnMenuContext){
.next_index = 0,
.str_buffer = {0},
.input_queue = furi_message_queue_alloc(1, sizeof(JsBtnMenuEvent)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ButtonMenu* input, JsBtnMenuContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)button_menu_alloc,
.free = (JsViewFree)button_menu_free,
.get_view = (JsViewGetView)button_menu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_button_menu_add_child,
.reset_children = (JsViewResetChildren)js_button_menu_reset_children,
.prop_cnt = 1,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)matrix_header_assign},
}};
JS_GUI_VIEW_DEF(button_menu, &view_descriptor);

View File

@@ -0,0 +1,274 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/button_panel.h>
#include <toolbox/str_buffer.h>
typedef struct {
size_t matrix_x, matrix_y;
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsBtnPanelContext;
typedef struct {
int32_t index;
InputType input_type;
} JsBtnPanelEvent;
static const char* js_input_type_to_str(InputType type) {
switch(type) {
case InputTypePress:
return "press";
case InputTypeRelease:
return "release";
case InputTypeShort:
return "short";
case InputTypeLong:
return "long";
case InputTypeRepeat:
return "repeat";
default:
furi_crash();
}
}
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnPanelContext* context) {
UNUSED(context);
JsBtnPanelEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("index", mjs_mk_number(mjs, event.index));
JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false));
}
return event_obj;
}
static void input_callback(void* ctx, int32_t index, InputType type) {
JsBtnPanelContext* context = ctx;
JsBtnPanelEvent event = {
.index = index,
.input_type = type,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool matrix_size_x_assign(
struct mjs* mjs,
ButtonPanel* panel,
JsViewPropValue value,
JsBtnPanelContext* context) {
UNUSED(mjs);
context->matrix_x = value.number;
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
return true;
}
static bool matrix_size_y_assign(
struct mjs* mjs,
ButtonPanel* panel,
JsViewPropValue value,
JsBtnPanelContext* context) {
UNUSED(mjs);
context->matrix_y = value.number;
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
return true;
}
static bool js_button_panel_add_child(
struct mjs* mjs,
ButtonPanel* panel,
JsBtnPanelContext* context,
mjs_val_t child_obj) {
typedef enum {
JsButtonPanelChildTypeButton,
JsButtonPanelChildTypeLabel,
JsButtonPanelChildTypeIcon,
} JsButtonPanelChildType;
static const JsValueEnumVariant js_button_panel_child_type_variants[] = {
{"button", JsButtonPanelChildTypeButton},
{"label", JsButtonPanelChildTypeLabel},
{"icon", JsButtonPanelChildTypeIcon},
};
static const JsValueDeclaration js_button_panel_child_type =
JS_VALUE_ENUM(JsButtonPanelChildType, js_button_panel_child_type_variants);
static const JsValueDeclaration js_button_panel_number = JS_VALUE_SIMPLE(JsValueTypeInt32);
static const JsValueObjectField js_button_panel_common_fields[] = {
{"type", &js_button_panel_child_type},
{"x", &js_button_panel_number},
{"y", &js_button_panel_number},
};
static const JsValueDeclaration js_button_panel_common =
JS_VALUE_OBJECT(js_button_panel_common_fields);
static const JsValueDeclaration js_button_panel_pointer =
JS_VALUE_SIMPLE(JsValueTypeRawPointer);
static const JsValueObjectField js_button_panel_button_fields[] = {
{"matrixX", &js_button_panel_number},
{"matrixY", &js_button_panel_number},
{"icon", &js_button_panel_pointer},
{"iconSelected", &js_button_panel_pointer},
};
static const JsValueDeclaration js_button_panel_button =
JS_VALUE_OBJECT(js_button_panel_button_fields);
static const JsValueDeclaration js_button_panel_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField js_button_panel_label_fields[] = {
{"text", &js_button_panel_string},
{"font", &js_gui_font_declaration},
};
static const JsValueDeclaration js_button_panel_label =
JS_VALUE_OBJECT(js_button_panel_label_fields);
static const JsValueObjectField js_button_panel_icon_fields[] = {
{"icon", &js_button_panel_pointer},
};
static const JsValueDeclaration js_button_panel_icon =
JS_VALUE_OBJECT(js_button_panel_icon_fields);
JsButtonPanelChildType child_type;
int32_t x, y;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_common),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&child_type,
&x,
&y);
if(status != JsValueParseStatusOk) return false;
switch(child_type) {
case JsButtonPanelChildTypeButton: {
int32_t matrix_x, matrix_y;
const Icon *icon, *icon_selected;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_button),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&matrix_x,
&matrix_y,
&icon,
&icon_selected);
if(status != JsValueParseStatusOk) return false;
button_panel_add_item(
panel,
context->next_index++,
matrix_x,
matrix_y,
x,
y,
icon,
icon_selected,
(ButtonItemCallback)input_callback,
context);
break;
}
case JsButtonPanelChildTypeLabel: {
const char* text;
Font font;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_label),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&text,
&font);
if(status != JsValueParseStatusOk) return false;
button_panel_add_label(
panel, x, y, font, str_buffer_make_owned_clone(&context->str_buffer, text));
break;
}
case JsButtonPanelChildTypeIcon: {
const Icon* icon;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_icon),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&icon);
if(status != JsValueParseStatusOk) return false;
button_panel_add_icon(panel, x, y, icon);
break;
}
}
return true;
}
static void js_button_panel_reset_children(ButtonPanel* panel, JsBtnPanelContext* context) {
context->next_index = 0;
button_panel_reset(panel);
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsBtnPanelContext* ctx_make(struct mjs* mjs, ButtonPanel* panel, mjs_val_t view_obj) {
UNUSED(panel);
JsBtnPanelContext* context = malloc(sizeof(JsBtnPanelContext));
*context = (JsBtnPanelContext){
.matrix_x = 1,
.matrix_y = 1,
.next_index = 0,
.str_buffer = {0},
.input_queue = furi_message_queue_alloc(1, sizeof(JsBtnPanelEvent)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ButtonPanel* input, JsBtnPanelContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)button_panel_alloc,
.free = (JsViewFree)button_panel_free,
.get_view = (JsViewGetView)button_panel_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_button_panel_add_child,
.reset_children = (JsViewResetChildren)js_button_panel_reset_children,
.prop_cnt = 2,
.props = {
(JsViewPropDescriptor){
.name = "matrixSizeX",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)matrix_size_x_assign},
(JsViewPropDescriptor){
.name = "matrixSizeY",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)matrix_size_y_assign},
}};
JS_GUI_VIEW_DEF(button_panel, &view_descriptor);

View File

@@ -14,9 +14,19 @@ typedef struct {
.name = #icon, .data = &I_##icon \
}
#define ANIM_ICON_DEF(icon) \
(IconDefinition) { \
.name = #icon, .data = &A_##icon \
}
static const IconDefinition builtin_icons[] = {
ICON_DEF(DolphinWait_59x54),
ICON_DEF(js_script_10px),
ICON_DEF(off_19x20),
ICON_DEF(off_hover_19x20),
ICON_DEF(power_19x20),
ICON_DEF(power_hover_19x20),
ANIM_ICON_DEF(Settings_14),
};
// Firmware's Icon struct needs a frames array, and uses a small CompressHeader

View File

@@ -31,6 +31,14 @@ typedef struct {
void* custom_data;
} JsGuiViewData;
static const JsValueEnumVariant js_gui_font_variants[] = {
{"primary", FontPrimary},
{"secondary", FontSecondary},
{"keyboard", FontKeyboard},
{"bit_numbers", FontBigNumbers},
};
const JsValueDeclaration js_gui_font_declaration = JS_VALUE_ENUM(Font, js_gui_font_variants);
/**
* @brief Transformer for custom events
*/
@@ -273,9 +281,12 @@ static bool
/**
* @brief Sets the list of children. Not available from JS.
*/
static bool
js_gui_view_internal_set_children(struct mjs* mjs, mjs_val_t children, JsGuiViewData* data) {
data->descriptor->reset_children(data->specific_view, data->custom_data);
static bool js_gui_view_internal_set_children(
struct mjs* mjs,
mjs_val_t children,
JsGuiViewData* data,
bool do_reset) {
if(do_reset) data->descriptor->reset_children(data->specific_view, data->custom_data);
for(size_t i = 0; i < mjs_array_length(mjs, children); i++) {
mjs_val_t child = mjs_array_get(mjs, children, i);
@@ -357,7 +368,7 @@ static void js_gui_view_set_children(struct mjs* mjs) {
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
js_gui_view_internal_set_children(mjs, children, data);
js_gui_view_internal_set_children(mjs, children, data, true);
}
/**
@@ -450,7 +461,7 @@ static void js_gui_vf_make_with(struct mjs* mjs) {
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
if(!js_gui_view_internal_set_children(mjs, children, data)) return;
if(!js_gui_view_internal_set_children(mjs, children, data, false)) return;
}
mjs_return(mjs, view_obj);

View File

@@ -20,6 +20,11 @@ typedef union {
mjs_val_t term;
} JsViewPropValue;
/**
* JS-to-C font enum mapping
*/
extern const JsValueDeclaration js_gui_font_declaration;
/**
* @brief Assigns a value to a view property
*

View File

@@ -1,4 +1,5 @@
#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*))));
API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*)),
API_VARIABLE(js_gui_font_declaration, const JsValueDeclaration)));

View File

@@ -0,0 +1,105 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/menu.h>
#include <toolbox/str_buffer.h>
typedef struct {
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsMenuCtx;
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);
}
static void choose_callback(void* context, uint32_t index) {
JsMenuCtx* ctx = context;
furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk);
}
static bool
js_menu_add_child(struct mjs* mjs, Menu* menu, JsMenuCtx* context, mjs_val_t child_obj) {
static const JsValueDeclaration js_menu_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueDeclaration js_menu_pointer = JS_VALUE_SIMPLE(JsValueTypeRawPointer);
static const JsValueObjectField js_menu_child_fields[] = {
{"icon", &js_menu_pointer},
{"label", &js_menu_string},
};
static const JsValueDeclaration js_menu_child = JS_VALUE_OBJECT(js_menu_child_fields);
const Icon* icon;
const char* label;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_menu_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&icon,
&label);
if(status != JsValueParseStatusOk) return false;
menu_add_item(
menu,
str_buffer_make_owned_clone(&context->str_buffer, label),
icon,
context->next_index++,
choose_callback,
context);
return true;
}
static void js_menu_reset_children(Menu* menu, JsMenuCtx* context) {
context->next_index = 0;
menu_reset(menu);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsMenuCtx* ctx_make(struct mjs* mjs, Menu* input, mjs_val_t view_obj) {
UNUSED(input);
JsMenuCtx* context = malloc(sizeof(JsMenuCtx));
context->queue = furi_message_queue_alloc(1, 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(Menu* input, JsMenuCtx* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)menu_alloc,
.free = (JsViewFree)menu_free,
.get_view = (JsViewGetView)menu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_menu_add_child,
.reset_children = (JsViewResetChildren)js_menu_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(menu, &view_descriptor);

View File

@@ -0,0 +1,130 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/number_input.h>
typedef struct {
int32_t default_val, min_val, max_val;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsNumKbdContext;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsNumKbdContext* context) {
UNUSED(context);
int32_t number;
furi_check(furi_message_queue_get(queue, &number, 0) == FuriStatusOk);
return mjs_mk_number(mjs, number);
}
static void input_callback(void* ctx, int32_t value) {
JsNumKbdContext* context = ctx;
furi_check(furi_message_queue_put(context->input_queue, &value, 0) == FuriStatusOk);
}
static bool header_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
UNUSED(context);
number_input_set_header_text(input, value.string);
return true;
}
static bool min_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->min_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static bool max_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->max_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static bool default_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->default_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static JsNumKbdContext* ctx_make(struct mjs* mjs, NumberInput* input, mjs_val_t view_obj) {
JsNumKbdContext* context = malloc(sizeof(JsNumKbdContext));
*context = (JsNumKbdContext){
.default_val = 0,
.max_val = 100,
.min_val = 0,
.input_queue = furi_message_queue_alloc(1, sizeof(int32_t)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(NumberInput* input, JsNumKbdContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)number_input_alloc,
.free = (JsViewFree)number_input_free,
.get_view = (JsViewGetView)number_input_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 4,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "minValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)min_val_assign},
(JsViewPropDescriptor){
.name = "maxValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)max_val_assign},
(JsViewPropDescriptor){
.name = "defaultValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)default_val_assign},
}};
JS_GUI_VIEW_DEF(number_input, &view_descriptor);

View File

@@ -0,0 +1,102 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/popup.h>
#include <toolbox/str_buffer.h>
typedef struct {
StrBuffer str_buffer;
FuriSemaphore* semaphore;
JsEventLoopContract contract;
} JsPopupCtx;
static void timeout_callback(JsPopupCtx* context) {
furi_check(furi_semaphore_release(context->semaphore) == FuriStatusOk);
}
static bool
header_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_header(
popup,
str_buffer_make_owned_clone(&context->str_buffer, value.string),
64,
0,
AlignCenter,
AlignTop);
return true;
}
static bool
text_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_text(
popup,
str_buffer_make_owned_clone(&context->str_buffer, value.string),
64,
32,
AlignCenter,
AlignCenter);
return true;
}
static bool
timeout_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_timeout(popup, value.number);
popup_enable_timeout(popup);
return true;
}
static JsPopupCtx* ctx_make(struct mjs* mjs, Popup* popup, mjs_val_t view_obj) {
JsPopupCtx* context = malloc(sizeof(JsPopupCtx));
context->semaphore = furi_semaphore_alloc(1, 0);
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = context->semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
},
};
mjs_set(mjs, view_obj, "timeout", ~0, mjs_mk_foreign(mjs, &context->contract));
popup_set_callback(popup, (PopupCallback)timeout_callback);
popup_set_context(popup, context);
return context;
}
static void ctx_destroy(Popup* popup, JsPopupCtx* context, FuriEventLoop* loop) {
UNUSED(popup);
furi_event_loop_maybe_unsubscribe(loop, context->semaphore);
furi_semaphore_free(context->semaphore);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)popup_alloc,
.free = (JsViewFree)popup_free,
.get_view = (JsViewGetView)popup_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 = "text",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)text_assign},
(JsViewPropDescriptor){
.name = "timeout",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)timeout_assign},
}};
JS_GUI_VIEW_DEF(popup, &view_descriptor);

View File

@@ -6,6 +6,7 @@
#define QUEUE_LEN 2
typedef struct {
int32_t next_index;
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsSubmenuCtx;
@@ -30,18 +31,24 @@ static bool
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.term);
for(size_t i = 0; i < len; i++) {
mjs_val_t item = mjs_array_get(mjs, value.term, i);
if(!mjs_is_string(item)) return false;
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
}
static bool js_submenu_add_child(
struct mjs* mjs,
Submenu* submenu,
JsSubmenuCtx* context,
mjs_val_t child_obj) {
const char* str = mjs_get_string(mjs, &child_obj, NULL);
if(!str) return false;
submenu_add_item(submenu, str, context->next_index++, choose_callback, context);
return true;
}
static void js_submenu_reset_children(Submenu* submenu, JsSubmenuCtx* context) {
context->next_index = 0;
submenu_reset(submenu);
}
static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) {
UNUSED(input);
JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx));
@@ -73,15 +80,13 @@ static const JsViewDescriptor view_descriptor = {
.get_view = (JsViewGetView)submenu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 2,
.add_child = (JsViewAddChild)js_submenu_add_child,
.reset_children = (JsViewResetChildren)js_submenu_reset_children,
.prop_cnt = 1,
.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,163 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/variable_item_list.h>
#include <toolbox/str_buffer.h>
typedef struct {
StrBuffer str_buffer;
// let mjs do the memory management heavy lifting, store children in a js array
struct mjs* mjs;
mjs_val_t children;
VariableItemList* list;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsViListContext;
typedef struct {
int32_t item_index;
int32_t value_index;
} JsViListEvent;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsViListContext* context) {
UNUSED(context);
JsViListEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("itemIndex", mjs_mk_number(mjs, event.item_index));
JS_FIELD("valueIndex", mjs_mk_number(mjs, event.value_index));
}
return event_obj;
}
static void js_vi_list_change_callback(VariableItem* item) {
JsViListContext* context = variable_item_get_context(item);
struct mjs* mjs = context->mjs;
uint8_t item_index = variable_item_list_get_selected_item_index(context->list);
uint8_t value_index = variable_item_get_current_value_index(item);
// type safety ensured in add_child
mjs_val_t variants = mjs_array_get(mjs, context->children, item_index);
mjs_val_t variant = mjs_array_get(mjs, variants, value_index);
variable_item_set_current_value_text(item, mjs_get_string(mjs, &variant, NULL));
JsViListEvent event = {
.item_index = item_index,
.value_index = value_index,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool js_vi_list_add_child(
struct mjs* mjs,
VariableItemList* list,
JsViListContext* context,
mjs_val_t child_obj) {
static const JsValueDeclaration js_vi_list_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueDeclaration js_vi_list_arr = JS_VALUE_SIMPLE(JsValueTypeAnyArray);
static const JsValueDeclaration js_vi_list_int_default_0 =
JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 0);
static const JsValueObjectField js_vi_list_child_fields[] = {
{"label", &js_vi_list_string},
{"variants", &js_vi_list_arr},
{"defaultSelected", &js_vi_list_int_default_0},
};
static const JsValueDeclaration js_vi_list_child =
JS_VALUE_OBJECT_W_DEFAULTS(js_vi_list_child_fields);
JsValueParseStatus status;
const char* label;
mjs_val_t variants;
int32_t default_selected;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_vi_list_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&label,
&variants,
&default_selected);
if(status != JsValueParseStatusOk) return false;
size_t variants_cnt = mjs_array_length(mjs, variants);
for(size_t i = 0; i < variants_cnt; i++)
if(!mjs_is_string(mjs_array_get(mjs, variants, i))) return false;
VariableItem* item = variable_item_list_add(
list,
str_buffer_make_owned_clone(&context->str_buffer, label),
variants_cnt,
js_vi_list_change_callback,
context);
variable_item_set_current_value_index(item, default_selected);
mjs_val_t default_variant = mjs_array_get(mjs, variants, default_selected);
variable_item_set_current_value_text(item, mjs_get_string(mjs, &default_variant, NULL));
mjs_array_push(context->mjs, context->children, variants);
return true;
}
static void js_vi_list_reset_children(VariableItemList* list, JsViListContext* context) {
mjs_disown(context->mjs, &context->children);
context->children = mjs_mk_array(context->mjs);
mjs_own(context->mjs, &context->children);
variable_item_list_reset(list);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsViListContext* ctx_make(struct mjs* mjs, VariableItemList* list, mjs_val_t view_obj) {
JsViListContext* context = malloc(sizeof(JsViListContext));
*context = (JsViListContext){
.str_buffer = {0},
.mjs = mjs,
.children = mjs_mk_array(mjs),
.list = list,
.input_queue = furi_message_queue_alloc(1, sizeof(JsViListEvent)),
};
mjs_own(context->mjs, &context->children);
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "valueUpdate", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(VariableItemList* input, JsViListContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)variable_item_list_alloc,
.free = (JsViewFree)variable_item_list_free,
.get_view = (JsViewGetView)variable_item_list_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_vi_list_add_child,
.reset_children = (JsViewResetChildren)js_vi_list_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(vi_list, &view_descriptor);

View File

@@ -0,0 +1,40 @@
/**
* Displays a list of buttons.
*
* <img src="../images/button_menu.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let buttonMenuView = require("gui/button_menu");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Textual header above the buttons
*
* @version Added in JS SDK 0.4
* @module
*/
import type { View, ViewFactory, InputType } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string;
};
type Child = { type: "common" | "control", label: string };
declare class ButtonMenu extends View<Props, Child> {
input: Contract<{ index: number, type: InputType }>;
}
declare class ButtonMenuFactory extends ViewFactory<Props, Child, ButtonMenu> { }
declare const factory: ButtonMenuFactory;
export = factory;

View File

@@ -0,0 +1,49 @@
/**
* Displays a button matrix.
*
* <img src="../images/button_panel.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let buttonPanelView = require("gui/button_panel");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `matrixSizeX`: Width of imaginary grid used for navigation
* - `matrixSizeY`: Height of imaginary grid used for navigation
*
* @version Added in JS SDK 0.4
* @module
*/
import type { View, ViewFactory, Font, InputType } from ".";
import type { Contract } from "../event_loop";
import { IconData } from "./icon";
type Props = {
matrixSizeX: number,
matrixSizeY: number,
};
type Position = { x: number, y: number };
type ButtonChild = { type: "button", matrixX: number, matrixY: number, icon: IconData, iconSelected: IconData } & Position;
type LabelChild = { type: "label", font: Font, text: string } & Position;
type IconChild = { type: "icon", icon: IconData };
type Child = ButtonChild | LabelChild | IconChild;
declare class ButtonPanel extends View<Props, Child> {
input: Contract<{ index: number, type: InputType }>;
}
declare class ButtonPanelFactory extends ViewFactory<Props, Child, ButtonPanel> { }
declare const factory: ButtonPanelFactory;
export = factory;

View File

@@ -1,4 +1,7 @@
export type BuiltinIcon = "DolphinWait_59x54" | "js_script_10px";
export type BuiltinIcon = "DolphinWait_59x54" | "js_script_10px"
| "off_19x20" | "off_hover_19x20"
| "power_19x20" | "power_hover_19x20"
| "Settings_14";
export type IconData = symbol & { "__tag__": "icon" };
// introducing a nominal type in a hacky way; the `__tag__` property doesn't really exist.

View File

@@ -24,24 +24,23 @@
* ### View
* In Flipper's terminology, a "View" is a fullscreen design element that
* assumes control over the entire viewport and all input events. Different
* types of views are available (not all of which are unfortunately currently
* implemented in JS):
* types of views are available:
* | View | Has JS adapter? |
* |----------------------|-----------------------|
* | `button_menu` | |
* | `button_panel` | |
* | `button_menu` | |
* | `button_panel` | |
* | `byte_input` | ✅ |
* | `dialog_ex` | ✅ (as `dialog`) |
* | `empty_screen` | ✅ |
* | `file_browser` | ✅ (as `file_picker`) |
* | `loading` | ✅ |
* | `menu` | |
* | `number_input` | |
* | `popup` | |
* | `menu` | |
* | `number_input` | |
* | `popup` | |
* | `submenu` | ✅ |
* | `text_box` | ✅ |
* | `text_input` | ✅ |
* | `variable_item_list` | |
* | `variable_item_list` | ✅ (as `vi_list`) |
* | `widget` | ✅ |
*
* In JS, each view has its own set of properties (or just "props"). The
@@ -119,6 +118,9 @@
import type { Contract } from "../event_loop";
export type Font = "primary" | "secondary" | "keyboard" | "big_numbers";
export type InputType = "press" | "release" | "short" | "long" | "repeat";
type Properties = { [K: string]: any };
export declare class View<Props extends Properties, Child> {

View File

@@ -0,0 +1,38 @@
/**
* A list of selectable entries consisting of an icon and a label.
*
* <img src="../images/menu.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let submenuView = require("gui/menu");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the GUI example.
*
* # View props
* This view doesn't have any props.
*
* @version Added in JS SDK 0.1
* @version API changed in JS SDK 0.4
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
import type { IconData } from "./icon";
type Props = {};
type Child = { icon: IconData, label: string };
declare class Submenu extends View<Props, Child> {
chosen: Contract<number>;
}
declare class SubmenuFactory extends ViewFactory<Props, Child, Submenu> { }
declare const factory: SubmenuFactory;
export = factory;

View File

@@ -0,0 +1,44 @@
/**
* Displays a number input keyboard.
*
* <img src="../images/number_input.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let numberInputView = require("gui/number_input");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Text displayed at the top of the screen
* - `minValue`: Minimum allowed numeric value
* - `maxValue`: Maximum allowed numeric value
* - `defaultValue`: Default numeric value
*
* @version Added in JS SDK 0.4
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
minValue: number,
maxValue: number,
defaultValue: number,
}
type Child = never;
declare class NumberInput extends View<Props, Child> {
input: Contract<number>;
}
declare class NumberInputFactory extends ViewFactory<Props, Child, NumberInput> { }
declare const factory: NumberInputFactory;
export = factory;

View File

@@ -0,0 +1,43 @@
/**
* Like a Dialog, but with a built-in timer.
*
* <img src="../images/popup.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let popupView = require("gui/popup");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Text displayed in bold at the top of the screen
* - `text`: Text displayed in the middle of the string
* - `timeout`: Timeout, in milliseconds, after which the event will fire. The
* timer starts counting down when this property is assigned.
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
text: string,
timeout: number,
}
type Child = never;
declare class Popup extends View<Props, Child> {
timeout: Contract;
}
declare class PopupFactory extends ViewFactory<Props, Child, Popup> { }
declare const factory: PopupFactory;
export = factory;

View File

@@ -18,9 +18,9 @@
*
* # View props
* - `header`: Text displayed at the top of the screen in bold
* - `items`: Array of selectable textual items
*
* @version Added in JS SDK 0.1
* @version API changed in JS SDK 0.4
* @module
*/
@@ -29,9 +29,8 @@ import type { Contract } from "../event_loop";
type Props = {
header: string,
items: string[],
};
type Child = never;
type Child = string;
declare class Submenu extends View<Props, Child> {
chosen: Contract<number>;
}

View File

@@ -0,0 +1,38 @@
/**
* Displays a list of settings-like variable items.
*
* <img src="../images/vi_list.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let viListView = require("gui/vi_list");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* This view doesn't have any props
*
* @version Added in JS SDK 0.4
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {};
type Child = { label: string, variants: string[] };
declare class ViList extends View<Props, Child> {
valueUpdate: Contract<{ itemIndex: number, valueIndex: number }>;
}
declare class ViListFactory extends ViewFactory<Props, Child, ViList> { }
declare const factory: ViListFactory;
export = factory;

View File

@@ -24,4 +24,4 @@
"prompts": "^2.4.2",
"serialport": "^12.0.0"
}
}
}

View File

@@ -29,6 +29,7 @@ env.Append(
File("float_tools.h"),
File("value_index.h"),
File("tar/tar_archive.h"),
File("str_buffer.h"),
File("stream/stream.h"),
File("stream/file_stream.h"),
File("stream/string_stream.h"),

18
lib/toolbox/str_buffer.c Normal file
View File

@@ -0,0 +1,18 @@
#include "str_buffer.h"
const char* str_buffer_make_owned_clone(StrBuffer* buffer, const char* str) {
char* owned = strdup(str);
buffer->n_owned_strings++;
buffer->owned_strings =
realloc(buffer->owned_strings, buffer->n_owned_strings * sizeof(const char*)); // -V701
buffer->owned_strings[buffer->n_owned_strings - 1] = owned;
return owned;
}
void str_buffer_clear_all_clones(StrBuffer* buffer) {
for(size_t i = 0; i < buffer->n_owned_strings; i++) {
free(buffer->owned_strings[i]);
}
free(buffer->owned_strings);
buffer->owned_strings = NULL;
}

47
lib/toolbox/str_buffer.h Normal file
View File

@@ -0,0 +1,47 @@
/**
* @file str_buffer.h
*
* Allows you to create an owned clone of however many strings that you need,
* then free all of them at once. Essentially the simplest possible append-only
* unindexable array of owned C-style strings.
*/
#pragma once
#include <furi.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief StrBuffer instance
*
* Place this struct directly wherever you want, it doesn't have to be `alloc`ed
* and `free`d.
*/
typedef struct {
char** owned_strings;
size_t n_owned_strings;
} StrBuffer;
/**
* @brief Makes a owned duplicate of the provided string
*
* @param[in] buffer StrBuffer instance
* @param[in] str Input C-style string
*
* @returns C-style string that contains to be valid event after `str` becomes
* invalid
*/
const char* str_buffer_make_owned_clone(StrBuffer* buffer, const char* str);
/**
* @brief Clears all owned duplicates
*
* @param[in] buffer StrBuffer instance
*/
void str_buffer_clear_all_clones(StrBuffer* buffer);
#ifdef __cplusplus
}
#endif

View File

@@ -173,6 +173,7 @@ Header,+,lib/toolbox/protocols/protocol_dict.h,,
Header,+,lib/toolbox/pulse_protocols/pulse_glue.h,,
Header,+,lib/toolbox/saved_struct.h,,
Header,+,lib/toolbox/simple_array.h,,
Header,+,lib/toolbox/str_buffer.h,,
Header,+,lib/toolbox/stream/buffered_file_stream.h,,
Header,+,lib/toolbox/stream/file_stream.h,,
Header,+,lib/toolbox/stream/stream.h,,
@@ -2632,6 +2633,8 @@ Function,+,storage_simply_remove,_Bool,"Storage*, const char*"
Function,+,storage_simply_remove_recursive,_Bool,"Storage*, const char*"
Function,-,stpcpy,char*,"char*, const char*"
Function,-,stpncpy,char*,"char*, const char*, size_t"
Function,+,str_buffer_clear_all_clones,void,StrBuffer*
Function,+,str_buffer_make_owned_clone,const char*,"StrBuffer*, const char*"
Function,+,strcasecmp,int,"const char*, const char*"
Function,-,strcasecmp_l,int,"const char*, const char*, locale_t"
Function,+,strcasestr,char*,"const char*, const char*"
1 entry status name type params
173 Header + lib/toolbox/pulse_protocols/pulse_glue.h
174 Header + lib/toolbox/saved_struct.h
175 Header + lib/toolbox/simple_array.h
176 Header + lib/toolbox/str_buffer.h
177 Header + lib/toolbox/stream/buffered_file_stream.h
178 Header + lib/toolbox/stream/file_stream.h
179 Header + lib/toolbox/stream/stream.h
2633 Function + storage_simply_remove_recursive _Bool Storage*, const char*
2634 Function - stpcpy char* char*, const char*
2635 Function - stpncpy char* char*, const char*, size_t
2636 Function + str_buffer_clear_all_clones void StrBuffer*
2637 Function + str_buffer_make_owned_clone const char* StrBuffer*, const char*
2638 Function + strcasecmp int const char*, const char*
2639 Function - strcasecmp_l int const char*, const char*, locale_t
2640 Function + strcasestr char* const char*, const char*

View File

@@ -245,6 +245,7 @@ Header,+,lib/toolbox/protocols/protocol_dict.h,,
Header,+,lib/toolbox/pulse_protocols/pulse_glue.h,,
Header,+,lib/toolbox/saved_struct.h,,
Header,+,lib/toolbox/simple_array.h,,
Header,+,lib/toolbox/str_buffer.h,,
Header,+,lib/toolbox/stream/buffered_file_stream.h,,
Header,+,lib/toolbox/stream/file_stream.h,,
Header,+,lib/toolbox/stream/stream.h,,
@@ -3322,6 +3323,8 @@ Function,+,storage_simply_remove,_Bool,"Storage*, const char*"
Function,+,storage_simply_remove_recursive,_Bool,"Storage*, const char*"
Function,-,stpcpy,char*,"char*, const char*"
Function,-,stpncpy,char*,"char*, const char*, size_t"
Function,+,str_buffer_clear_all_clones,void,StrBuffer*
Function,+,str_buffer_make_owned_clone,const char*,"StrBuffer*, const char*"
Function,+,strcasecmp,int,"const char*, const char*"
Function,-,strcasecmp_l,int,"const char*, const char*, locale_t"
Function,+,strcasestr,char*,"const char*, const char*"
1 entry status name type params
245 Header + lib/toolbox/pulse_protocols/pulse_glue.h
246 Header + lib/toolbox/saved_struct.h
247 Header + lib/toolbox/simple_array.h
248 Header + lib/toolbox/str_buffer.h
249 Header + lib/toolbox/stream/buffered_file_stream.h
250 Header + lib/toolbox/stream/file_stream.h
251 Header + lib/toolbox/stream/stream.h
3323 Function + storage_simply_remove_recursive _Bool Storage*, const char*
3324 Function - stpcpy char* char*, const char*
3325 Function - stpncpy char* char*, const char*, size_t
3326 Function + str_buffer_clear_all_clones void StrBuffer*
3327 Function + str_buffer_make_owned_clone const char* StrBuffer*, const char*
3328 Function + strcasecmp int const char*, const char*
3329 Function - strcasecmp_l int const char*, const char*, locale_t
3330 Function + strcasestr char* const char*, const char*