Application (FmrbApp)¶
FmrbApp is the application base class for Family mruby. User apps must inherit from FmrbApp and implement lifecycle methods.
Minimal Example¶
class MyApp < FmrbApp
def on_create
clear_user_area(FmrbGfx::WHITE)
@gfx.draw_text(@user_area_x0 + 4, @user_area_y0 + 4,
"Hello, mruby!", FmrbGfx::BLACK)
draw_window_frame
@gfx.present
end
def on_update
100 # Wait 100ms
end
end
MyApp.new.start
Window size and other settings are specified in a .toml file (see App Configuration File (.toml)).
Lifecycle¶
| Method | When Called | Return Value Meaning |
|---|---|---|
on_create |
Once when the app starts | Any (ignored) |
on_update |
Repeatedly within the main loop | Wait time in milliseconds until the next on_update. Default 330ms |
on_event(ev) |
On keyboard / mouse / gamepad / HID input | Any |
on_suspend |
When switching to a fullscreen app | Any |
on_resume |
When returning from a suspended state | Any |
on_resize(w, h) |
When the window changes size — a drag on the corner, or a switch to or from fullscreen. fullscreen? and the user area are already updated |
Any |
on_quit_request |
On Ctrl + Q, instead of closing outright |
Any |
on_destroy |
Once when the app exits | Any |
start
+-- on_create
+-- main_loop:
+-- on_update -> wait for return value ms via _spin
+-- _spin dispatches on_event(ev), _handle_system_control(msg)
+-- repeats until the app stops (`running?` turns false)
+-- destroy -> on_destroy
on_update return value
A short value (10-30ms) increases the frame rate but consumes more CPU. For games, 16-33ms is a good target; for static UIs, 100-500ms is appropriate.
Event Handling (on_event(ev))¶
ev is a Hash, and you determine the event type with ev[:type].
Keyboard¶
def on_event(ev)
case ev[:type]
when :key_down
keycode = ev[:keycode] # Character code (platform-dependent)
scancode = ev[:scancode] # USB HID Usage ID (platform-independent)
modifier = ev[:modifier] # Modifier key bits (see below)
char = ev[:character] # Character (if available)
Log.info("key down: #{char.inspect}")
when :key_up
# ...
end
end
Modifier key bits (ev[:modifier]) layout:
| Bit | Value | Meaning |
|---|---|---|
| 0 | 0x01 | LSHIFT |
| 1 | 0x02 | RSHIFT |
| 2 | 0x04 | LCTRL |
| 3 | 0x08 | RCTRL |
| 4 | 0x10 | LALT |
| 5 | 0x20 | RALT |
Helper methods are provided:
ev_ctrl?(ev) # Ctrl is pressed
ev_shift?(ev) # Shift is pressed
ev_alt?(ev) # Alt is pressed
Note
Use scancode when identifying character keys. keycode values vary between platforms (e.g. SDL2 returns ASCII values).
FmrbConst::KEY_* / MOD_* constants
Since scancode values are USB HID Usage IDs, you can use constants like FmrbConst::KEY_ESC instead of writing raw values like 0x29 (ESC). Modifier key mask constants such as FmrbConst::MOD_CTRL are also available. See Constants > KEY_ / MOD_ for a full list.
Mouse¶
when :mouse_down, :mouse_up
ev[:button] # 1=left, 2=middle, 3=right
ev[:x] # X coordinate within the window
ev[:y] # Y coordinate within the window
when :mouse_move
ev[:x], ev[:y]
Clicks on the title bar (left-click to close / right-click to reload) are handled by the base class before your on_event runs, so there is nothing to call: an on_event that never calls super still closes and reloads. Apps written against 2.0 often open with super(ev); since 2.1 that reaches an empty method and does nothing, so it can stay or go.
Wheel¶
A wheel event reaches the window that has the keyboard, not the one under the pointer. Two
helpers read it, and both return nil when the event is not a wheel, so an app reads one
of them and moves on:
rows = wheel_rows(ev)
if rows
@scroll -= rows
redraw
end
| Method | |
|---|---|
wheel_rows(ev) |
Notches multiplied by the machine's wheel_lines setting. For anything whose rows are text rows |
wheel_notches(ev) |
The raw notch count. For a list whose rows are not text rows — the launcher's tiles are several lines tall, and wheel_lines sends them flying |
How far a notch reaches is the machine's setting, not each app's opinion.
Gamepad¶
when :gamepad_down, :gamepad_up
ev[:gamepad_id] # 0 and above
ev[:button] # 0..15
when :gamepad_axis
ev[:gamepad_id]
ev[:axis] # 0..5
ev[:value] # Axis value
FmrbConst::GP_* constants
Button numbers have constants like FmrbConst::GP_SQUARE / GP_CROSS / GP_START, and axis numbers have GP_AXIS_LX / GP_AXIS_LY, etc. See Constants > GP_* for details.
Window Operations¶
| Method | Purpose |
|---|---|
set_window_position(x, y) |
Change the window position |
draw_window_frame |
Draw the window frame (title bar + border). Reuses a GfxBlock managed by the base class |
clear_user_area(color = FmrbConst::THEME_WINDOW_BG) |
Fill the drawable area (excluding title bar and border). The default follows the system theme, and the call redraws the window frame and marks any attached widgets |
request_fullscreen(on) / toggle_fullscreen |
Switch between windowed and fullscreen. The VM keeps running, so app state survives; the answer arrives as on_resize, with fullscreen? and the user area already updated |
request_file_select(mode = "open") |
Invoke the system file selection dialog |
sync_file(path, dest: nil) |
Make the graphics/audio side's copy of a file match this one, transferring it only when it differs. A headless app can do this too |
request_reload |
Reload the script (automatically called on title bar right-click) |
Scrollbars moved: draw_scrollbar and scrollbar_hit are gone, and a scrollbar is now a
widget — see UI Widgets.
Use clear_user_area instead of @gfx.clear
@gfx.clear(color) fills the entire canvas, which also erases the title bar and close button. To preserve the window frame, use clear_user_area(color) instead.
Messaging¶
| Method | Purpose |
|---|---|
subscribe(topic) / unsubscribe(topic) |
Subscribe to a topic |
publish(topic, data=nil) |
Send to a topic |
send_message(dest_pid, msg_type, data) |
Direct message to the kernel or a specific app. data is automatically serialized via MessagePack |
For details and receive handlers, see Pub/Sub.
Timers¶
| Method | |
|---|---|
set_timer(interval) { ... } |
Run the block once, interval ms from now. Returns an id |
clear_time(id) |
Cancel one that has not fired |
A timer is one-shot: to repeat, set the next one from inside the block. They are checked
once per turn of the app loop, so the resolution is whatever on_update returns.
Starting another app¶
request_run(path, prev_pid = nil) asks the kernel to spawn a file, optionally killing an
instance a previous request started. The answer arrives as an app-control message with the
new pid (nil when it failed). Paths are limited to /app and /home.
Extra canvases¶
create_canvas_gfx(width:, height:, z_offset: 1, transparent: false, transparent_color: 0)
returns an FmrbGfx bound to a canvas of the app's own, and delete_canvas_gfx(gfx)
releases it (they also go when the app does, including on a crash). Position and show it
with gfx.present(x, y); combine it with
set_viewport for a hardware-scrolled layer.
This is for fullscreen apps: the window manager does not follow extra canvases across a change of focus.
Collecting garbage while idle¶
self.idle_gc = true splits collection into steps taken while the app has nothing to do,
instead of stopping it for 100-200 ms in the middle of something. It is for an app that
must not pause — a player, an animation — and cannot get its allocation down to nothing.
The costs: generational mode goes off and does not come back on its own, and a step can delay a message by its own length. An app that is always busy gets the old behaviour back by itself.
Execution Control¶
| Method | Purpose |
|---|---|
start |
Starts the event loop (on_create is called). running? is true from here |
stop |
Ends it: running? turns false, and destroy follows after the next _spin |
destroy |
Notifies the kernel of exit, calls @gfx.destroy, on_destroy, _cleanup |
| on_quit_request | Called on Ctrl + Q instead of stopping the app outright. The default is to close; override it to ask first when there is unsaved work |
| request_early_update | End the current wait now, so the loop reaches on_update without waiting out the timeout the app asked for |
Normally, writing just MyApp.new.start is sufficient.
request_early_update is what lets an idle app sleep for a long time at all: without a way
out, "sleep until the next deadline" has to be capped at whatever the app might later be
asked to do in a hurry, which is a poll by another name. It only means anything from inside
a callback — on_control, on_event — because from on_update the next sleep is about to
be recomputed anyway.
Colours from the theme¶
Five readers give an app the system colours without naming a single number, so it follows the machine's theme and the user's colour overrides:
| Method | Role |
|---|---|
theme_bg |
The page background |
theme_fg |
Ink on theme_bg |
theme_accent |
Selection, emphasis |
theme_border |
Rules, boxes, muted text |
theme_fg_light |
Ink on the accent or on a button |
What the app can read¶
| Description | |
|---|---|
@gfx (or gfx) |
FmrbGfx instance (drawing API; nil in headless mode) |
name |
App display name (from .toml app_screen_name) |
platform |
:esp32 or :linux |
running? |
true while the app is running |
fullscreen? |
true in fullscreen mode |
closable? |
Whether a click on the close button may stop the app. closable = false turns it off, for an app that owns the screen |
rounded_corners? |
Whether this window's corners are rounded, for an app drawing its own frame |
@window_width / @window_height |
Overall window size |
@pos_x / @pos_y |
Absolute coordinates of the window's top-left corner |
@user_area_x0 / @user_area_y0 / @user_area_x1 / @user_area_y1 |
Boundaries of the drawable area, excluding title bar and borders |
@user_area_width / @user_area_height |
Size of the drawable area |
Names beginning with @_ belong to the base class
Since 2.1 the base keeps its own state in @_-prefixed variables, so anything you
assign that does not start with @_ is yours alone. @running and @name were the two
that hurt most — an app that set its own @running used to exit silently — and they are
now read through running? and name.
The sound chip is not one of these: an app makes its own with FmrbAudio.new(self).
Drawing within the window frame
In windowed mode with a title bar, always draw within the @user_area_* bounds. Start from @user_area_x0, @user_area_y0 and stay within @user_area_width and @user_area_height.
File and Directory Paths¶
Pass root-relative paths (e.g. /home/foo.txt) or SD card paths like /mnt/sd/... directly to File.open / Dir.open. See File & I/O > File Namespace for details.
Class Methods¶
| Method | Purpose |
|---|---|
FmrbApp.language |
The UI language the user chose, "en" or "ja" |
FmrbApp.ps |
Array of Hash showing all process states (id, name, state, vm_type, mem_*, stack_water, etc.) |
FmrbApp.config(section) |
Read a specified section from the app's .toml |
FmrbApp.wallclock |
Current time ({year, month, day, hour, minute, second}) |
FmrbApp.set_wallclock(year, month, day, hour, minute, second) |
Set RTC / system time |
FmrbApp.gfx_stats |
Drawing statistics {cmds:, presents:} |
FmrbApp.sys_pool_info |
System memory pool information |
FmrbApp.pool_used |
How many bytes of its own pool this app has used, or -1. Subtract it across a piece of work to see the garbage that work made |
FmrbApp.heap_info |
ESP-IDF heap info (free, total, min_free, largest_block, etc.) |
FmrbApp.enable_cursor |
Show mouse cursor (delayed until the first mouse movement) |
FmrbApp.set_cursor_visible(visible) |
Immediately show/hide cursor. Useful for hiding in fullscreen games and restoring on exit |
FmrbApp.uptime_us |
Microseconds since boot |
FmrbApp.wifi_info / FmrbApp.wifi_connected? |
The network the machine is on, and whether it is on one |
FmrbApp.usb_devices |
What is plugged into the USB host port |
FmrbApp.set_kana_mode(mode) |
Switch kana input: 0 ASCII, 1 hiragana, 2 katakana. The change comes back as a kana_mode event |
FmrbApp.reboot |
Restart the machine |
FmrbApp._get_last_error |
Last app error (returns {name:, error:} if present) |
Constants¶
| Constant | Value | Purpose |
|---|---|---|
TITLE_BAR_H |
11 | Title bar height (px) |
CORNER_R |
4 | Window corner radius |
TRANSPARENT_COLOR |
0x01 | Transparent color (passes through during compositing) |
SCROLLBAR_W |
10 | Scrollbar width |
SCROLLBAR_BTN_H |
10 | Scrollbar button height |
Example: Increment a Counter on Button Press¶
class CounterApp < FmrbApp
def on_create
@count = 0
redraw
end
def on_event(ev)
super # Inherit close button handling
if ev[:type] == :mouse_down && ev[:button] == 1
@count += 1
redraw
elsif ev[:type] == :key_down && ev[:character] == "r"
@count = 0
redraw
end
end
def on_update
300
end
private
def redraw
clear_user_area(FmrbGfx::WHITE)
@gfx.draw_text(@user_area_x0 + 4, @user_area_y0 + 4,
"Count: #{@count}", FmrbGfx::BLACK)
draw_window_frame
@gfx.present
end
end
CounterApp.new.start