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, readelm-guide.mdinstead — 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
- 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:
#[derive(Clone, Debug)]
enum MyMsg { Pick(u64), Clear /* …one per change… */ }
Step 5 — implement Elm
Add this block (fill the match):
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:
#[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.