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.

routes.rs
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.

scoped_state.rs
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 notice

A 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.

frame.rs
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.

intents.rs
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)?;