Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

.nornir/elm-guide.md

The Elm architecture — guide

Class: guide · audience: a techie writing or driving a facett component. The what is in elm-design.md; the why in elm-reasoning.md. This file is the workflow.

f0 — zero to a tested component (the whole workflow)

Four moves turn a struct into a harness-testable facett component:

  1. Name the state. One struct = your Model: #[derive(Serialize, Deserialize, Clone, PartialEq)]. Everything you'd assert on goes here; caches/animation do not (put those in Cell/RefCell fields on the view, not in Model).
  2. Name the inputs. One enum Msg: Clone + Debug. Every click, key, drag = a variant.
  3. impl Elm. update(&mut self, Msg) is your only mutation; view(&self, ui) paints and returns the Msgs the frame produced. No I/O in update — return Effect data (or use enum Effect {} if there is none).
  4. One macro line. facett_core::impl_facet_via_elm!(MyView); — now it's a Facet.

Then it is automatically testable headless (see f3). That's the payoff: no window in the test.

f1 — filling the trait

struct MyView { state: MyModel, /* + non-Model caches: */ layout: RefCell<Option<Layout>> }

impl facett_core::Elm for MyView {
    type Model = MyModel;
    type Msg = MyMsg;
    type Effect = ();                       // or a real enum; or `enum NoFx {}` for "pure"
    fn title(&self) -> &str { "my view" }
    fn state(&self) -> &MyModel { &self.state }
    fn update(&mut self, m: MyMsg) -> Vec<()> {
        match m { MyMsg::Pick(id) => self.state.selected = Some(id), /* … */ }
        vec![]
    }
    fn view(&self, ui: &mut egui::Ui) -> Vec<MyMsg> {
        let mut out = vec![];
        if ui.button("pick").clicked() { out.push(MyMsg::Pick(some_id)); }
        out                                  // never mutate self here
    }
}

Rule of thumb: if you're about to write &mut self anywhere but update, stop — that state change is a Msg.

f2 — choosing the macro form

you need… use
just a Facet from your Elm impl_facet_via_elm!(MyView)
that plus extra Facet methods (but keep default state_json) impl_facet_via_elm!(MyView, { fn foo(&self){…} })
a richer state_json than serde(state()) impl_facet_via_elm!(MyView, custom_state_json, { fn state_json(&self)->Value {…} })

Most components use form 1 or the custom_state_json form (a fuller introspection json).

f3 — driving it in a test (the whole point)

use facett_core::harness;
let mut v = MyView::new();
// Apply a Msg script and inspect the resulting Model — NO window, NO GPU:
let model = harness::snapshot(&mut v, [MyMsg::Pick(id_a), MyMsg::Pick(id_b)]);
assert_eq!(model.selected, Some(id_b));
// Golden-file style:
let json = harness::snapshot_json(&mut v, [MyMsg::Pick(id_a)]);
// Assert it actually paints (offscreen), for the render-report path:
assert!(harness::headless_render(&mut v).drew());

drive returns the Effects so you can assert "this Msg requested effect X" without X happening. This is the same seam nornir's test-matrix and robot functions consume at scale — see nornir test-function design.

Look & feel

A component inherits facett's look automatically: paint through the egui::Ui you're given and use facett_core::look/Theme (the harness has render_themed to snapshot a component under a specific Theme). Don't hard-code colours; pull from the theme so every component reads as one product.