Each recipe is the state it needs, what render adds, and what event does.
Ask before destroying
// state
confirming: Option<String>, // the path about to be deleted
// render: a child of the page or pane, while it is set
if let Some(path) = &self.confirming {
page = page.child(
Sheet::new(tr("Delete This File?"))
.id("confirm")
.message(path.as_str())
.action(Button::new("confirm.cancel", tr("Cancel")))
.action(Button::new("confirm.delete", tr("Delete")).destructive()),
);
}
// event
"confirm" | "confirm.cancel" => self.confirming = None,
"confirm.delete" => {
if let Some(path) = self.confirming.take() {
self.delete(&path);
}
}
Name the destructive button for what it destroys. Where the thing can be put back (a trash), do not ask at all.
A menu on right click
// state
menu: Option<(String, (f64, f64))>, // what it is about, and where
// render
Row::new(name).id(format!("item:{id}")).context()
// …
if let Some((_, point)) = &self.menu {
page = page.child(
Menu::new("menu", *point)
.item(MenuItem::new("menu.open", tr("Open")))
.separator()
.item(MenuItem::new("menu.delete", tr("Move to Trash")).destructive()),
);
}
// event
id if id.starts_with("item:") && event.is("context") => {
self.menu = event.point().map(|point| (id["item:".len()..].to_owned(), point));
}
"menu" => self.menu = None, // dismissed
"menu.open" => {
if let Some((item, _)) = self.menu.take() {
self.open(&item);
}
}
Keep menus short, grouped by separators, with the destructive item last.
Loading, then content, or an error
enum Networks {
Loading,
Ready(Vec<Network>),
Failed(String),
}
// render
match &*self.networks.lock().unwrap() {
Networks::Loading => Group::new().header(tr("Networks")).loading(true).into(),
Networks::Ready(list) if list.is_empty() => EmptyState::new(tr("No Networks in Range"))
.icon("wifi-off")
.body(tr("Move closer to a router, or check that Wi-Fi is on."))
.into(),
Networks::Ready(list) => self.list(list),
Networks::Failed(why) => Banner::new(BannerTone::Error, tr("Could Not Look for Networks"))
.body(why.as_str())
.action(Button::new("retry", tr("Try Again")))
.into(),
}
The thread that fills it calls handle.invalidate() when the state changes
(see Background work). All four arms are
designed states, not afterthoughts.
Search as you type
// render
TextField::new("search", self.query.as_str()).placeholder(tr("Search")).icon("search").autofocus()
// event
"search" => self.query = event.text().unwrap_or_default().to_owned(),
Filter in render. Key the result rows by what they are, so that rows which
stay keep their place as the list narrows. If matching is slow, do it on a
thread and show the previous results meanwhile; never an empty list between
two keystrokes.
Several pages with a sidebar
// state
section: Section, // an enum of your pages
// render
SplitView::new()
.sidebar_items(Section::ALL.iter().map(|section| {
SidebarItem::new(format!("nav:{}", section.id()), section.title())
.icon(section.icon())
.selected(*section == self.section)
.into()
}))
.child(match self.section {
Section::General => self.general(),
Section::Sound => self.sound(),
})
// event
id if id.starts_with("nav:") => self.section = Section::from_id(&id[4..]),
Give each Page a .key(…) of its own, so that going from one to another
plays a page’s entrance instead of morphing one page into the next.
A switch that reflects the system
// render: what *is*, not what was asked for
Row::new(tr("Bluetooth")).trailing(Switch::new("bluetooth", self.snapshot.powered).label(tr("Bluetooth")))
// event: ask, and let the next snapshot tell the truth
"bluetooth" => {
let on = event.flag().unwrap_or(false);
let handle = self.handle.clone();
let failed = self.failed.clone();
std::thread::spawn(move || {
if let Err(why) = set_powered(on) {
*failed.lock().unwrap() = Some(why);
}
handle.invalidate();
});
}
If it fails, the switch goes back by itself on the next scene, and a banner says why.
Clearing a field from code
A field that has the caret keeps its own text. To change it from the applet:
self.entry.clear();
self.revision += 1;
// …
TextField::new("entry", self.entry.as_str()).revision(self.revision)
Opening a file the applet is started with
// main: what it was launched with
let target = std::env::var("NIRVANA_OPEN").ok().filter(|target| !target.is_empty());
// event: launched again while already running
"@host" if event.is("open") => {
if let Some(target) = event.text() {
self.open(std::path::Path::new(target));
}
}
Closing itself
"done" => commands.close_window(),
From a thread: handle.close_window().