Skip to main content

Module ch2_state

Module ch2_state 

Source
Expand description

§2. State and events

The Chapter 1 window has a title but no mutable data. Add a todo list, render one row per item, and let a click toggle that item.

§Give the view some state

A view’s state is the data in its fields. Add a Todo type and give Todos a list of them, with a few starter items:

use gpui::SharedString;

struct Todo {
    title: SharedString,
    done: bool,
}

struct Todos {
    todos: Vec<Todo>,
}

impl Todos {
    fn new() -> Self {
        Self {
            todos: vec![
                Todo { title: "Buy milk".into(), done: false },
                Todo { title: "Water the plants".into(), done: true },
                Todo { title: "Call Sam".into(), done: false },
            ],
        }
    }
}

SharedString is GPUI’s immutable string type for displayed text. Cloning it is cheap, which matters because render rebuilds the element tree whenever the view changes.

Create the view with Todos::new() in main:

cx.open_window(WindowOptions::default(), |_window, cx| cx.new(|_cx| Todos::new()))
    .unwrap();

§Render a row per todo

render maps the state to elements. children adds one child for each item produced by an iterator:

impl Render for Todos {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);

        div()
            .size_full()
            .p_8()
            .bg(colors.background)
            .text_color(colors.text)
            .child(div().text_3xl().child("Todos"))
            .children(self.todos.iter().enumerate().map(|(index, todo)| {
                div()
                    .id(index)
                    .when(todo.done, |row| row.line_through())
                    .child(todo.title.clone())
                    .on_click(cx.listener(move |todos, _event, _window, cx| {
                        let todo = &mut todos.todos[index];
                        todo.done = !todo.done;
                        cx.notify();
                    }))
            }))
    }
}

Run it and click a task. The handler flips done, notifies the view, and the next render adds or removes the strikethrough.

§What happens when you click

The click reaches the redraw through three linked pieces:

id makes the element interactive. Clicking involves state that lasts longer than one frame: GPUI has to remember that the mouse went down on this row before it can report a click when the mouse comes up. Elements are rebuilt on every frame, so GPUI keeps that state for you, keyed by the element’s id. Only elements with an id have methods like on_click. Ids must be unique among their siblings and stable from one frame to the next. The row’s index works for now.

cx.listener connects a handler to the view. An element’s event handler receives the event, the window and the app, but not your view: the element doesn’t know which view created it. Context::listener wraps your closure so that it also receives &mut Todos, the view that was rendering. Inside it you can change the view’s fields directly.

cx.notify() asks for a redraw. GPUI does not watch your fields. When an event handler changes state that affects what’s on screen, call notify. GPUI then renders the window again before the next frame, and your render method draws the todo in its new state. Forgetting to notify is the most common reason a GPUI view doesn’t update.

when styles conditionally. when applies a closure to the element only if a condition holds, so you can keep building the element in one expression.

§The program so far

src/main.rs
use gpui::{App, Context, QuitMode, SharedString, Window, WindowOptions, colors::Colors, div, prelude::*};

struct Todo {
    title: SharedString,
    done: bool,
}

struct Todos {
    todos: Vec<Todo>,
}

impl Todos {
    fn new() -> Self {
        Self {
            todos: vec![
                Todo { title: "Buy milk".into(), done: false },
                Todo { title: "Water the plants".into(), done: true },
                Todo { title: "Call Sam".into(), done: false },
            ],
        }
    }
}

impl Render for Todos {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);

        div()
            .size_full()
            .p_8()
            .bg(colors.background)
            .text_color(colors.text)
            .child(div().text_3xl().child("Todos"))
            .children(self.todos.iter().enumerate().map(|(index, todo)| {
                div()
                    .id(index)
                    .when(todo.done, |row| row.line_through())
                    .child(todo.title.clone())
                    .on_click(cx.listener(move |todos, _event, _window, cx| {
                        let todo = &mut todos.todos[index];
                        todo.done = !todo.done;
                        cx.notify();
                    }))
            }))
    }
}

fn main() {
    gpui_platform::application()
        .with_quit_mode(QuitMode::LastWindowClosed)
        .run(|cx: &mut App| {
            cx.open_window(WindowOptions::default(), |_window, cx| cx.new(|_cx| Todos::new()))
                .unwrap();
            cx.activate(true);
        });
}

The state now changes through the full event path. The rows are still an unstructured stack, so Chapter 3 gives that same tree a usable layout and derives the unfinished count from the list. In the next chapter, you use the same render function to add structure, spacing, and a derived count.