[FL-3893] JS modules (#3841)
* feat: backport js_gpio from unleashed
* feat: backport js_keyboard, TextInputModel::minimum_length from unleashed
* fix: api version inconsistency
* style: js_gpio
* build: fix submodule ._ .
* refactor: js_gpio
* docs: type declarations for gpio
* feat: gpio interrupts
* fix: js_gpio freeing, resetting and minor stylistic changes
* style: js_gpio
* style: mlib array, fixme's
* feat: js_gpio adc
* feat: js_event_loop
* docs: js_event_loop
* feat: js_event_loop subscription cancellation
* feat: js_event_loop + js_gpio integration
* fix: js_event_loop memory leak
* feat: stop event loop on back button
* test: js: basic, math, event_loop
* feat: js_event_loop queue
* feat: js linkage to previously loaded plugins
* build: fix ci errors
* feat: js module ordered teardown
* feat: js_gui_defer_free
* feat: basic hourglass view
* style: JS ASS (Argument Schema for Scripts)
* fix: js_event_loop mem leaks and lifetime problems
* fix: crashing test and pvs false positives
* feat: mjs custom obj destructors, gui submenu view
* refactor: yank js_gui_defer_free (yuck)
* refactor: maybe_unsubscribe
* empty_screen, docs, typing fix-ups
* docs: navigation event & demo
* feat: submenu setHeader
* feat: text_input
* feat: text_box
* docs: text_box availability
* ci: silence irrelevant pvs low priority warning
* style: use furistring
* style: _get_at -> _safe_get
* fix: built-in module name assignment
* feat: js_dialog; refactor, optimize: js_gui
* docs: js_gui
* ci: silence pvs warning: Memory allocation is infallible
* style: fix storage spelling
* feat: foreign pointer signature checks
* feat: js_storage
* docs: js_storage
* fix: my unit test was breaking other tests ;_;
* ci: fix ci?
* Make doxygen happy
* docs: flipper, math, notification, global
* style: review suggestions
* style: review fixups
* fix: badusb demo script
* docs: badusb
* ci: add nofl
* ci: make linter happy
* Bump api version
Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
2024-10-14 21:42:11 +03:00
|
|
|
# js_gui {#js_gui}
|
|
|
|
|
|
|
|
|
|
# GUI module
|
|
|
|
|
```js
|
|
|
|
|
let eventLoop = require("event_loop");
|
|
|
|
|
let gui = require("gui");
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This module depends on the `event_loop` module, so it _must_ only be imported
|
|
|
|
|
after `event_loop` is imported.
|
|
|
|
|
|
|
|
|
|
## Conceptualizing GUI
|
|
|
|
|
### Event loop
|
|
|
|
|
It is highly recommended to familiarize yourself with the event loop first
|
|
|
|
|
before doing GUI-related things.
|
|
|
|
|
|
|
|
|
|
### Canvas
|
|
|
|
|
The canvas is just a drawing area with no abstractions over it. Drawing on the
|
|
|
|
|
canvas directly (i.e. not through a viewport) is useful in case you want to
|
|
|
|
|
implement a custom design element, but this is rather uncommon.
|
|
|
|
|
|
|
|
|
|
### Viewport
|
|
|
|
|
A viewport is a window into a rectangular portion of the canvas. Applications
|
|
|
|
|
always access the canvas through a viewport.
|
|
|
|
|
|
|
|
|
|
### 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):
|
2025-02-13 12:50:38 +04:00
|
|
|
| View | Has JS adapter? |
|
|
|
|
|
|----------------------|-----------------------|
|
|
|
|
|
| `button_menu` | ❌ |
|
|
|
|
|
| `button_panel` | ❌ |
|
|
|
|
|
| `byte_input` | ✅ |
|
|
|
|
|
| `dialog_ex` | ✅ (as `dialog`) |
|
|
|
|
|
| `empty_screen` | ✅ |
|
|
|
|
|
| `file_browser` | ✅ (as `file_picker`) |
|
|
|
|
|
| `loading` | ✅ |
|
|
|
|
|
| `menu` | ❌ |
|
|
|
|
|
| `number_input` | ❌ |
|
|
|
|
|
| `popup` | ❌ |
|
|
|
|
|
| `submenu` | ✅ |
|
|
|
|
|
| `text_box` | ✅ |
|
|
|
|
|
| `text_input` | ✅ |
|
|
|
|
|
| `variable_item_list` | ❌ |
|
|
|
|
|
| `widget` | ✅ |
|
[FL-3893] JS modules (#3841)
* feat: backport js_gpio from unleashed
* feat: backport js_keyboard, TextInputModel::minimum_length from unleashed
* fix: api version inconsistency
* style: js_gpio
* build: fix submodule ._ .
* refactor: js_gpio
* docs: type declarations for gpio
* feat: gpio interrupts
* fix: js_gpio freeing, resetting and minor stylistic changes
* style: js_gpio
* style: mlib array, fixme's
* feat: js_gpio adc
* feat: js_event_loop
* docs: js_event_loop
* feat: js_event_loop subscription cancellation
* feat: js_event_loop + js_gpio integration
* fix: js_event_loop memory leak
* feat: stop event loop on back button
* test: js: basic, math, event_loop
* feat: js_event_loop queue
* feat: js linkage to previously loaded plugins
* build: fix ci errors
* feat: js module ordered teardown
* feat: js_gui_defer_free
* feat: basic hourglass view
* style: JS ASS (Argument Schema for Scripts)
* fix: js_event_loop mem leaks and lifetime problems
* fix: crashing test and pvs false positives
* feat: mjs custom obj destructors, gui submenu view
* refactor: yank js_gui_defer_free (yuck)
* refactor: maybe_unsubscribe
* empty_screen, docs, typing fix-ups
* docs: navigation event & demo
* feat: submenu setHeader
* feat: text_input
* feat: text_box
* docs: text_box availability
* ci: silence irrelevant pvs low priority warning
* style: use furistring
* style: _get_at -> _safe_get
* fix: built-in module name assignment
* feat: js_dialog; refactor, optimize: js_gui
* docs: js_gui
* ci: silence pvs warning: Memory allocation is infallible
* style: fix storage spelling
* feat: foreign pointer signature checks
* feat: js_storage
* docs: js_storage
* fix: my unit test was breaking other tests ;_;
* ci: fix ci?
* Make doxygen happy
* docs: flipper, math, notification, global
* style: review suggestions
* style: review fixups
* fix: badusb demo script
* docs: badusb
* ci: add nofl
* ci: make linter happy
* Bump api version
Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
2024-10-14 21:42:11 +03:00
|
|
|
|
|
|
|
|
In JS, each view has its own set of properties (or just "props"). The programmer
|
|
|
|
|
can manipulate these properties in two ways:
|
|
|
|
|
- Instantiate a `View` using the `makeWith(props)` method, passing an object
|
|
|
|
|
with the initial properties
|
|
|
|
|
- Call `set(name, value)` to modify a property of an existing `View`
|
|
|
|
|
|
|
|
|
|
### View Dispatcher
|
|
|
|
|
The view dispatcher holds references to all the views that an application needs
|
|
|
|
|
and switches between them as the application makes requests to do so.
|
|
|
|
|
|
|
|
|
|
### Scene Manager
|
|
|
|
|
The scene manager is an optional add-on to the view dispatcher that makes
|
|
|
|
|
managing applications with complex navigation flows easier. It is currently
|
|
|
|
|
inaccessible from JS.
|
|
|
|
|
|
|
|
|
|
### Approaches
|
|
|
|
|
In total, there are three different approaches that you may take when writing
|
|
|
|
|
a GUI application:
|
|
|
|
|
| Approach | Use cases | Available from JS |
|
|
|
|
|
|----------------|------------------------------------------------------------------------------|-------------------|
|
|
|
|
|
| ViewPort only | Accessing the graphics API directly, without any of the nice UI abstractions | ❌ |
|
|
|
|
|
| ViewDispatcher | Common UI elements that fit with the overall look of the system | ✅ |
|
|
|
|
|
| SceneManager | Additional navigation flow management for complex applications | ❌ |
|
|
|
|
|
|
|
|
|
|
# Example
|
|
|
|
|
An example with three different views using the ViewDispatcher approach:
|
|
|
|
|
```js
|
|
|
|
|
let eventLoop = require("event_loop");
|
|
|
|
|
let gui = require("gui");
|
|
|
|
|
let loadingView = require("gui/loading");
|
|
|
|
|
let submenuView = require("gui/submenu");
|
|
|
|
|
let emptyView = require("gui/empty_screen");
|
|
|
|
|
|
|
|
|
|
// Common pattern: declare all the views in an object. This is absolutely not
|
|
|
|
|
// required, but adds clarity to the script.
|
|
|
|
|
let views = {
|
|
|
|
|
// the view dispatcher auto-✨magically✨ remembers views as they are created
|
|
|
|
|
loading: loadingView.make(),
|
|
|
|
|
empty: emptyView.make(),
|
|
|
|
|
demos: submenuView.makeWith({
|
|
|
|
|
items: [
|
|
|
|
|
"Hourglass screen",
|
|
|
|
|
"Empty screen",
|
|
|
|
|
"Exit app",
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// go to different screens depending on what was selected
|
|
|
|
|
eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, views) {
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
gui.viewDispatcher.switchTo(views.loading);
|
|
|
|
|
} else if (index === 1) {
|
|
|
|
|
gui.viewDispatcher.switchTo(views.empty);
|
|
|
|
|
} else if (index === 2) {
|
|
|
|
|
eventLoop.stop();
|
|
|
|
|
}
|
|
|
|
|
}, gui, eventLoop, views);
|
|
|
|
|
|
|
|
|
|
// go to the demo chooser screen when the back key is pressed
|
|
|
|
|
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, gui, views) {
|
|
|
|
|
gui.viewDispatcher.switchTo(views.demos);
|
|
|
|
|
}, gui, views);
|
|
|
|
|
|
|
|
|
|
// run UI
|
|
|
|
|
gui.viewDispatcher.switchTo(views.demos);
|
|
|
|
|
eventLoop.run();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
# API reference
|
|
|
|
|
## `viewDispatcher`
|
|
|
|
|
The `viewDispatcher` constant holds the `ViewDispatcher` singleton.
|
|
|
|
|
|
|
|
|
|
### `viewDispatcher.switchTo(view)`
|
|
|
|
|
Switches to a view, giving it control over the display and input
|
|
|
|
|
|
|
|
|
|
#### Parameters
|
|
|
|
|
- `view`: the `View` to switch to
|
|
|
|
|
|
|
|
|
|
### `viewDispatcher.sendTo(direction)`
|
|
|
|
|
Sends the viewport that the dispatcher manages to the front of the stackup
|
|
|
|
|
(effectively making it visible), or to the back (effectively making it
|
|
|
|
|
invisible)
|
|
|
|
|
|
|
|
|
|
#### Parameters
|
|
|
|
|
- `direction`: either `"front"` or `"back"`
|
|
|
|
|
|
|
|
|
|
### `viewDispatcher.sendCustom(event)`
|
|
|
|
|
Sends a custom number to the `custom` event handler
|
|
|
|
|
|
|
|
|
|
#### Parameters
|
|
|
|
|
- `event`: number to send
|
|
|
|
|
|
|
|
|
|
### `viewDispatcher.custom`
|
|
|
|
|
An event loop `Contract` object that identifies the custom event source,
|
|
|
|
|
triggered by `ViewDispatcher.sendCustom(event)`
|
|
|
|
|
|
|
|
|
|
### `viewDispatcher.navigation`
|
|
|
|
|
An event loop `Contract` object that identifies the navigation event source,
|
|
|
|
|
triggered when the back key is pressed
|
|
|
|
|
|
|
|
|
|
## `ViewFactory`
|
|
|
|
|
When you import a module implementing a view, a `ViewFactory` is instantiated.
|
|
|
|
|
For example, in the example above, `loadingView`, `submenuView` and `emptyView`
|
|
|
|
|
are view factories.
|
|
|
|
|
|
|
|
|
|
### `ViewFactory.make()`
|
|
|
|
|
Creates an instance of a `View`
|
|
|
|
|
|
|
|
|
|
### `ViewFactory.make(props)`
|
|
|
|
|
Creates an instance of a `View` and assigns initial properties from `props`
|
|
|
|
|
|
|
|
|
|
#### Parameters
|
|
|
|
|
- `props`: simple key-value object, e.g. `{ header: "Header" }`
|