Examples
One app, four views of it.
Each piece comes from the same notes app, which ships with the crate as examples/notes.rs.
A route table with a guard
Five screens, one of them under two parents. The trash is guarded by a plain function that says, in one sentence, why you can’t go in.
use carte::{Occlusion, Refusal, RouteTable, Scope, Store};
fn table() -> RouteTable {
let mut t = RouteTable::new();
t.at_root("home")
.under("note", "home")
.under("discard", "note")
.under("settings", "home")
.under("settings", "note") // one node, two parents
.under("trash", "home")
.occludes("discard", Occlusion::None) // the editor stays visible beneath
.guard("trash", trash_has_something);
t
}
// A guard is a plain function: it reads committed facts and either lets
// the walk through or says why not, in one sentence.
fn trash_has_something(store: &Store) -> Result<(), Refusal> {
match store.read::<Shared>(Scope::Process) {
Some(s) if !s.trashed.is_empty() => Ok(()),
_ => Err(Refusal::new("The trash is empty.")),
}
}
// The same sentence, ahead of time, for a greyed-out button:
// if let Err(why) = router.ask("trash", &store) { tooltip(why.sentence()) }State that lives with its screen
A scope keyed to a node mounts when the node joins the path, at the values its schema declares, and drops when it leaves.
use carte::schema::{Field, Kind, Lit, Schema};
use carte::{set, Outbox, Scope, Store};
#[derive(Clone, Debug, PartialEq)]
struct Editor {
title: String,
dirty: bool,
words: usize,
}
// Written by hand for now: the derive isn't in the crate yet.
impl Schema for Editor {
fn reflect() -> Kind {
Kind::Record(vec![
Field::new("title", Kind::Str).starting_at(Lit::Str("Untitled".into())),
Field::new("dirty", Kind::Bool),
Field::new("words", Kind::Int),
])
}
fn at_mount(_: Option<&Lit>) -> Self {
Self {
title: String::at_mount(Some(&Lit::Str("Untitled".into()))),
dirty: bool::at_mount(None),
words: usize::at_mount(None),
}
}
}
// Lives exactly as long as "note" is on the path.
const EDITOR: Scope = Scope::Node("note");
let mut store = Store::over(&table());
store.provides::<Editor>(EDITOR)?; // no initial value: the schema is it
let editor = store.producer::<Editor>(EDITOR, &["title", "dirty", "words"])?;
let watcher = store.subscriber();
store.subscribe(watcher, EDITOR, "dirty")?;
// Later, once a resolve has put "note" on the path:
let notice = store.tick(&mut outbox, |_actions, barrier| {
if let Ok(mut w) = barrier.writer(&editor) {
set!(w, dirty, true).unwrap();
set!(w, words, 42).unwrap();
}
});
assert!(notice.woke(watcher)); // two sets, one noticeA node and the frame
A node claims at most one child from your model. Each frame draws from committed state, ticks once, then re-derives the path.
use carte::{Departure, Escape, Node, Occlusion, Outbox, RouteId, Router, Store};
struct Home;
impl Node<Model, Action> for Home {
// At most one child, read from the model. This is the whole of routing.
fn resolve_child(&self, m: &Model) -> Option<RouteId> { m.home_child }
fn occludes(&self) -> Occlusion { Occlusion::View }
fn update(&mut self, _: &Model, _: &mut Outbox<Action>, _: f32) {}
fn escape(&mut self, _: &Model, _: &mut Outbox<Action>) -> Escape { Escape::Fall }
fn take_leave_request(&mut self) -> bool { false }
fn child_popped(&mut self, out: &mut Outbox<Action>, _: RouteId) {
out.push(Action::Closed { parent: "home" }); // a pop becomes an action
}
fn enter(&mut self, _: &mut Model) {}
fn leave(&mut self, _: &mut Model) {}
fn depart(&mut self, _: Option<RouteId>) -> Departure { Departure::Cut }
}
type Screen = dyn Node<Model, Action>;
let routes: Vec<(RouteId, Box<Screen>)> = vec![("home", Box::new(Home)) /* , … */];
let mut router = Router::new(table(), routes, |_: &Model| "home")?;
// Each frame, wherever your UI already draws (egui's `update`, say):
for id in router.visible_views() {
match id {
"home" => draw_home(&store, &mut outbox), // read committed, push actions
_ => {}
}
}
if escape_pressed {
router.escape(&model, &mut outbox);
}
store.tick(&mut outbox, |actions, barrier| {
for a in actions {
model.apply(a); // the only place the model changes
}
producers.produce(&model, barrier); // project the model into scopes
});
if router.resolve(&mut model, &mut store) {
// The path moved: fill the scopes that just mounted before drawing them.
store.tick(&mut outbox, |_, b| producers.produce(&model, b));
}Intents
A scope publishes what it accepts. Raises are checked against that vocabulary and arrive as typed actions on the outbox.
use carte::{Accepted, Intents, Outbox, Parameter, Raise, RaiseArg, Refused, Scope, Vocabulary};
enum NoteIntent {
Save,
Rename { title: String },
}
// What the "note" scope accepts, published like its fields.
impl Intents for NoteIntent {
fn vocabulary() -> Vocabulary {
Vocabulary::new(vec![
Accepted::new("save", vec![]),
Accepted::new("rename", vec![Parameter::of::<String>("title")]),
])
}
fn accept(raise: &Raise) -> Result<Self, Refused> {
match raise.name() {
"save" => {
raise.parameters("save", 0)?;
Ok(NoteIntent::Save)
}
"rename" => {
let mut p = raise.parameters("rename", 1)?;
Ok(NoteIntent::Rename { title: p.take::<String>("title")? })
}
other => Err(Refused::NoSuchIntent { intent: other.to_string() }),
}
}
}
store.accepts::<NoteIntent>(Scope::Node("note"))?;
// A view raises by name; the store checks it and pushes a typed action.
let raise = Raise::new("rename", vec![RaiseArg::Str("Groceries".into())]);
store.raise::<NoteIntent, NoteIntent>(Scope::Node("note"), &raise, &mut outbox)?;