The trait
pub trait Applet {
fn window_opened(&mut self, _window: &Window) {}
fn window_closed(&mut self, _window: &Window) {}
/// Describes the current UI of `window`. Must return quickly.
fn render(&mut self, window: &Window) -> Node;
/// Applies user input. Must return quickly.
fn event(&mut self, event: &Event, commands: &mut Commands);
}
run(|handle| MyApplet::new(handle)) starts the loop. It ends when the last
window closes or the runtime goes away.
One turn of the loop
- The person does something: clicks a button with the id
"save". - Nirvana sends the applet an event: node
save, nameclick. - The applet’s
eventchanges its state. It may also ask the host for something throughcommands(open a window, close this one). renderis called at once, and the scene goes back with the commands.- Nirvana patches the window from the old scene to the new one.
render is also called when a window opens, and whenever the applet says its
state changed by itself (Handle::invalidate).
There is no partial update and nothing to subscribe to. The whole scene is
described every time; working out what actually changed is Nirvana’s job.
Building a scene is cheap: it is plain data. Keep it that way: render must
not read files, run programs or wait for anything.
Ids: where events come from
A component that can be acted on takes an id. The event that comes back
carries it as event.node_id.
Button::new("save", tr("Save")).primary()
Row::new(tr("Wi-Fi")).id("open.wifi")
TextField::new("name", self.name.as_str())
Conventions the built-in applets follow:
- Dots or colons for families:
sheet.cancel,delete:3,key:7. - For lists, put what identifies the item in the id, and take it apart in
eventwithstrip_prefix:
if let Some(name) = event.node_id.strip_prefix("network:") {
self.join(name);
}
Ids must be unique within a window.
Keys: how things keep their identity
When a scene replaces another, Nirvana matches the new nodes to the old ones. A node with a key (or an id) is matched by it; nodes without are matched by position.
Give a key to anything in a list that can change:
Row::new(device.name.as_str()).key(format!("device:{}", device.address))
With a key, a row that moves is the same row: it slides to its new place, it keeps its focus, and its entrance does not play again. Without one, the third row simply becomes whatever is third now, and the list seems to flicker.
Use something stable for the key (an address, a path), not the index.
What survives a re-render
Because the window is patched and not rebuilt:
- Focus stays where it was.
- Scroll position stays.
- Text being typed stays: a field that has the caret is not overwritten
by the scene. If the applet changes a field’s text itself (a “clear”
button, keys on screen), it passes a
revisionthat changes each time:
TextField::new("entry", self.entry.as_str()).revision(self.revision)
- Live components (
Terminal,MediaView,VideoEditor,Editor) keep their own state between scenes: a terminal is not redrawn from the scene, it is fed bytes (streams).
The event
pub struct Event {
pub window_id: String,
pub node_id: String,
pub name: String, // "click", "change", "input", "submit", …
pub value: Option<Value>, // JSON; helpers below read it
}
Read the value with a helper instead of taking the JSON apart:
fn event(&mut self, event: &Event, commands: &mut Commands) {
match event.node_id.as_str() {
"enabled" => self.enabled = event.flag().unwrap_or(self.enabled),
"volume" => self.volume = event.number().unwrap_or(self.volume),
"name" => self.name = event.text().unwrap_or_default().to_owned(),
"tabs" => self.tab = event.index().unwrap_or(self.tab),
"close" => commands.close_window(),
_ => {}
}
}
event.is("submit") tells an Enter in a field from a keystroke (input).
Every component’s events and every helper are listed in
Events.
Events from the host
Two events come with the node id @host, from the runtime itself:
| Event | When |
|---|---|
open | The applet was started again with something to open (etyde-open images picture.png while the viewer runs). event.text() is the target. |
dismiss | A kept-alive window was put away. Reset what should not be there when it comes back: an emptied search field, a closed panel. |
Commands
What an applet may ask of the host while handling an event:
| Call | What happens |
|---|---|
commands.open_window(role, title, width, height) | Another window of this applet opens. render receives it with window.role set. |
commands.close_window() | The window the event came from closes. |
commands.hide_window() | The window is hidden and the applet keeps running (needs keepAlive). |
More in Windows and the registry.
Rules that keep it smooth
eventandrenderreturn quickly. Slow work goes on a thread, which callsHandle::invalidate()when it has something to show.- A control shows the real state. A switch for Wi-Fi shows whether Wi-Fi is on, not whether the switch was pressed. If turning it on fails, the next scene shows it off again, with the reason in a banner.
- No “Apply”. A toggle applies as it is pressed.
- Something is always shown. While a list loads, show the group with
loading(true)or rows with a spinner accessory; when it is empty, anEmptyStatethat says why.