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

nordisk/facett

.nornir/elm-design.md

The Elm architecture — design

Class: design · Function axis: f0 (the loop) + f1 trait · f2 bridge macro · f3 harness. Strict: this file states only the shape we run. Every why, every alternative, lives in elm-reasoning.md.

f0 — the loop (the whole)

A facett component is a value that owns all of its observable state and mutates it in exactly one place. The loop:

  1. view(&self, ui) paints from &self and returns Vec<Msg> — the events the frame produced. It takes &self: it cannot mutate. (facett-core/src/elm.rs:77)
  2. update(&mut self, msg) is the only &mut self method. It applies one Msg and returns Vec<Effect>. (facett-core/src/elm.rs:71)
  3. The Facet::ui bridge closes the loop: for m in self.view(ui) { self.update(m) }. It is written for you by the macro (f2), never by hand.

Four laws (the "FC" contract) make the loop hold; a component is conformant iff all hold:

law statement enforced by
FC-1/3 Model = all state you could observe or assert on type Model is the serialized truth (elm.rs:56)
FC-2 every input is a Msg; the writable surface is the Msg enum, nothing else update is the sole &mut (elm.rs:71)
FC-4/9 view is pure — same &self → same paint + same Msgs, no mutation view(&self, …) signature (elm.rs:77)
FC-5 selections/handles are stable ids, not indices Model fields hold ids
FC-7 ephemeral caches (layout, animation) live in Cell/RefCell outside Model not serialized; excluded from state()
FC-8 Effect is data; a component that does no I/O uses enum Effect {} (uninhabited) type Effect (elm.rs:61)

Reuse map (what the Elm subsystem uses): it depends only on egui (paint surface), serde (Model ↔ json), and facett's own Facet trait. It is used by every facett component crate (36 impl Elm today) and, downstream, by nornir's test-matrix and robot functions through f3 — see nornir test-function design.

f1 — the Elm trait

pub trait Elm (facett-core/src/elm.rs:54). Five members, nothing more:

type Model: Serialize + DeserializeOwned + Clone + PartialEq;  // :56  the observable truth
type Msg:   Clone + Debug;                                     // :59  the input alphabet
type Effect;                                                   // :61  side-work as data
fn title(&self) -> &str;                                       // :64
fn state(&self) -> &Self::Model;                               // :67  read-only view of Model
fn update(&mut self, msg: Self::Msg) -> Vec<Self::Effect>;     // :71  the only mutation
fn view(&self, ui: &mut egui::Ui) -> Vec<Self::Msg>;           // :77  pure paint → Msgs

Model: PartialEq is what makes snapshots diffable; DeserializeOwned is what lets the harness rehydrate a component from json.

f2 — the bridge macro impl_facet_via_elm!

macro_rules! impl_facet_via_elm (facett-core/src/elm.rs:131) writes impl Facet from an impl Elm: the title, the view→update ui loop, and a serde state_json. Three forms:

form invocation writes
1 impl_facet_via_elm!(MyView) (elm.rs:132) bridge + default state_json = serde(state())
2 impl_facet_via_elm!(MyView, { …overrides… }) (elm.rs:136) bridge + default state_json + extra Facet methods (must not redefine state_json)
3 impl_facet_via_elm!(MyView, custom_state_json, { fn state_json… }) (elm.rs:162) bridge with the default state_json suppressed, so the block supplies a richer one

f3 — the headless harness

facett-core/src/harness.rs drives a component with no window, no GPU:

fn signature use
drive drive<C: Elm>(&mut C, msgs) -> Vec<C::Effect> (harness.rs:219) apply a Msg script, collect Effects
snapshot snapshot<C: Elm>(&mut C, msgs) -> C::Model (:229) drive, then return the resulting Model
snapshot_json … -> serde_json::Value (:237) the Model as json (golden-file friendly)
state_json state_json<C: Elm>(&C) -> Value (:244) current Model as json, no drive
render_sized / headless_render / render_themed[_sized] (&mut dyn Facet, …) -> RenderReport (:71,:76,:81,:92) paint once into an off-screen surface; RenderReport::drew() (:20) asserts it painted

This is the seam every automated consumer uses: a Msg script in, a Model/RenderReport out, deterministically. Robot testing and the test-matrix are exactly this seam at scale.