~/
rust-munich
·
crux + gpui
# Step 1 — counter core We build the app in three iterations. The first one is the smallest thing crux can do: a counter that goes up and down. No weather, no HTTP, no async. File we work in: `core/src/lib.rs`. --- ## The three data types The three things every crux app starts with. Tiny on purpose. ```rust #[derive(Debug, Clone)] pub enum Event { Increment, Decrement, } #[derive(Default, Debug)] pub struct Model { pub count: i32, } #[derive(Debug, Clone, PartialEq)] pub struct ViewModel { pub count: i32, } ``` - `Event` — every thing that can happen. - `Model` — internal state. Only the core touches it. - `ViewModel` — what the UI sees. A deliberate copy of the model. Right now `Model` and `ViewModel` look identical. They will grow apart soon. --- ## The effect A crux app describes what it wants to happen with `Effect` values. At Step 1 we only need one: tell the UI to redraw. ```rust #[effect] pub enum Effect { Render(RenderOperation), } ``` `RenderOperation` ships with crux. We never construct it by hand — the `render()` helper does. --- ## The `App` trait crux tells us the shape our app must have. It's one trait with four associated types and two methods: ```rust pub trait App { type Event; type Model: Default; type ViewModel; type Effect; fn update( &self, event: Self::Event, model: &mut Self::Model, ) -> Command
; fn view(&self, model: &Self::Model) -> Self::ViewModel; } ``` The four associated types are the things we just defined. All we have to fill in is `update` and `view`. --- ## Implementing `App` for `CounterApp` `CounterApp` is a unit struct — it carries no state of its own. All state lives in the `Model` parameter that `update` mutates. ```rust #[derive(Default)] pub struct CounterApp; impl App for CounterApp { type Event = Event; type Model = Model; type ViewModel = ViewModel; type Effect = Effect; fn update(&self, event: Event, model: &mut Model) -> Command
{ /* … */ } fn view(&self, model: &Model) -> ViewModel { /* … */ } } ``` The associated types line up with our data types. The two methods are the only thing left to write. Let's look at each on its own. --- ## `update` ```rust fn update(&self, event: Event, model: &mut Model) -> Command
{ use crux_core::render::render; match event { Event::Increment => model.count += 1, Event::Decrement => model.count -= 1, } render() } ``` Read it as: *"Given the event and the current state, mutate the state and tell the runtime which effects to emit."* For now the only effect is `render()` — "redraw, please." --- ## `view` ```rust fn view(&self, model: &Model) -> ViewModel { ViewModel { count: model.count } } ``` `view` projects the private `Model` onto the public `ViewModel`. With just a counter it's a one-to-one copy; when we add weather in Step 3, this is where we hide internal flags the UI does not need. --- ## A test — and why it is boring (good) Because `update` is a plain function, the test needs no mocks, no runtime, no fixtures: ```rust #[test] fn increment_increases_count() { let app = CounterApp; let mut model = Model::default(); let _ = app.update(Event::Increment, &mut model); assert_eq!(model.count, 1); } ``` ```bash cargo test -p app_core ``` That's it for Step 1's core. Onwards to drawing it on screen.