Gunnar

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`](elm-reasoning.md).

![The Elm loop](assets/elm-loop.svg)

## 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 `Msg`s, 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](../../nornir/.nornir/test-design.md).

## f1 — the `Elm` trait

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

```rust
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.