Gunnar

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`](elm-design.md); the *why* in [`elm-reasoning.md`](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 `Msg`s 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

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

```rust
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 `Effect`s 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](../../nornir/.nornir/test-design.md).

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