Gunnar

nordisk/facett

.nornir/elm-for-idiots.md

# The Elm architecture — for idiots

> **Class:** `idiot`. Every step, nothing assumed. Goal: **migrate one existing facett
> component to the Elm shape**, from a cold shell to a green test. If you already know Rust,
> read [`elm-guide.md`](elm-guide.md) instead — this one spells out every keystroke.

## What you need first

- A terminal (the black window where you type commands).
- The facett repo on disk. In the terminal, type this and press **Enter**:
  ```
  cd /run/media/rickard/T9/git/facett
  ```

## Step 1 — pick a component and open it

1. In the terminal, type and press **Enter**:
   ```
   ls facett-*/src/lib.rs
   ```
   Each line is a component. Pick one that is **not** yet Elm — type and press **Enter**:
   ```
   grep -L "impl_facet_via_elm\|impl.*Elm for" facett-*/src/lib.rs
   ```
   Any file it prints is a candidate. Open one in your editor.

## Step 2 — find the state and the mutations

Read the file. Two questions:

- **"What does this thing remember?"** — the fields it reads when drawing. That is your **Model**.
- **"What changes those fields?"** — every place it writes `self.something = …`. Each is a **Msg**.

Write them down. You are going to move every write into one function.

## Step 3 — make the Model serializable

Above the state struct, make sure this line is there (add what's missing):
```
#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq)]
```
If a field can't be serialized (a GPU handle, an animation clock), it does **not** belong in
the Model — move it to a separate field on the *view* wrapped in `std::cell::RefCell`.

## Step 4 — write the Msg enum

Just under the struct, add one enum listing every change from Step 2:
```rust
#[derive(Clone, Debug)]
enum MyMsg { Pick(u64), Clear /* …one per change… */ }
```

## Step 5 — implement Elm

Add this block (fill the `match`):
```rust
impl facett_core::Elm for MyView {
    type Model = MyModel;
    type Msg = MyMsg;
    type Effect = ();                      // leave as () unless it starts threads / writes files
    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),
            MyMsg::Clear    => self.state.selected = None,
        }
        vec![]
    }
    fn view(&self, ui: &mut egui::Ui) -> Vec<MyMsg> {
        let mut out = Vec::new();
        // move your OLD drawing code here, but instead of `self.x = y` write `out.push(MyMsg::…)`
        out
    }
}
```
Two hard rules: **`view` never writes to `self`** (if the compiler complains it needs `&mut`,
you left a mutation in `view` — turn it into a pushed `Msg`). And **all your old writes now
live only inside `update`.**

## Step 6 — the one macro line

Right below the `impl Elm`, add:
```
facett_core::impl_facet_via_elm!(MyView);
```
Delete the old hand-written `impl Facet for MyView` if there was one (the macro writes it now).

## Step 7 — build it

Type and press **Enter**:
```
cargo build -p facett-<yourcomponent>
```
Red text = errors; fix what it names (usually a leftover `self.x =` inside `view`) and run it
again until it's quiet.

## Step 8 — write the headless test

At the bottom of the file add:
```rust
#[test]
fn pick_then_clear() {
    use facett_core::harness;
    let mut v = MyView::new();
    let model = harness::snapshot(&mut v, [MyMsg::Pick(7), MyMsg::Clear]);
    assert_eq!(model.selected, None);
}
```
Run it — type and press **Enter**:
```
cargo test -p facett-<yourcomponent>
```
Green `test result: ok` = done. You just tested a GUI component **without opening a window**.

## Step 9 — save your work

Type these three, pressing **Enter** after each:
```
git add -A
git commit -m "facett-<component>: migrate to Elm"
git push
```

That's the whole migration. One component, one commit. Do the next one the same way.