gpui-ce 0.3.0

A community fork of Zed's GPU-accelerated UI framework
Documentation
# 4. Text input

The list still contains only starter data. Add a text field above it, then bind
Enter to an action that appends its text to the list.

Three GPUI mechanisms meet here: an *entity* owns editing state, *focus* routes
keystrokes to that state, and an *action* gives Enter a name that can be bound in
the surrounding view.

## Add the text field

Text editing lives in a companion crate, `gpui_ce_elements`, so add it to
`Cargo.toml` next to GPUI:

```toml
gpui_ce_elements = { git = "https://github.com/gpui-ce/gpui-ce" }
```

A text field has state of its own: the text, the cursor, the selection, the undo
history. That state lives in an [`Entity`], an
`EditableTextState`,
which your view creates and keeps. Remove the starter todos, and create the
field's state instead:

```
# use gpui::{Context, Entity, Focusable, SharedString, Window, prelude::*};
use gpui_ce_elements::editable_text::{EditableTextState, StringStorage};

# struct Todo { title: SharedString, done: bool }
struct Todos {
    todos: Vec<Todo>,
    input: Entity<EditableTextState>,
}

impl Todos {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let input = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
        window.focus(&input.focus_handle(cx), cx);
        Self { todos: Vec::new(), input }
    }
}
```

`cx.new` works as it did in chapter 1: the app owns the field state and returns
an [`Entity`] handle. `Todos` keeps that handle so the action can read and clear
the field. Focusing its handle routes keyboard input there when the window opens.
At most one element in a window has focus at a time. Keystrokes go to that
element, and clicking another field moves focus there.

`new` now needs the window, so create the view with
`cx.new(|cx| Todos::new(window, cx))` in `main`.

Then show the field in `render`, right after the title. The
`text_input` element draws the
state you pass it:

```
# use gpui::{Entity, IntoElement, colors::Colors, prelude::*};
# use gpui_ce_elements::editable_text::{EditableTextState, text_input};
# fn field(input: &Entity<EditableTextState>, colors: &Colors) -> impl IntoElement {
text_input("new-todo")
    .state(input.downgrade())
    .placeholder("What needs doing?")
    .px_3()
    .py_2()
    .rounded_lg()
    .border_1()
    .border_color(colors.border)
# }
```

## Bind Enter to an action

Editing keys like Backspace and the arrows need bindings too. The text field
comes with a standard set; install them once, in `main`, before opening the
window:

```standalone_crate
# use gpui::App;
use gpui_ce_elements::editable_text::actions::{DEFAULT_INPUT_CONTEXT, default_bindings};

# fn setup(cx: &mut App) {
cx.bind_keys(default_bindings().as_keybindings(Some(DEFAULT_INPUT_CONTEXT)));
# }
```

Enter should add the todo, but the view should not inspect raw key events. GPUI
separates the operation from the key that invokes it. You declare the operation as
an [action](Action): a type
that names a command. Then you bind a key to it, and handle it in the view:

```standalone_crate
use gpui::{App, KeyBinding, actions};

actions!(todos, [AddTodo]);

fn bind_keys(cx: &mut App) {
    cx.bind_keys([KeyBinding::new("enter", AddTodo, Some("Todos"))]);
}
```

The third argument to [`KeyBinding::new`] is a *key context*. The binding applies
when the focused element is inside an element that declared that context.
Declare it on the view's root element, alongside the handler:

```standalone_crate
# use gpui::{Context, Entity, SharedString, Window, actions, div, prelude::*};
# use gpui_ce_elements::editable_text::EditableTextState;
# actions!(todos, [AddTodo]);
# struct Todo { title: SharedString, done: bool }
# struct Todos { todos: Vec<Todo>, input: Entity<EditableTextState> }
impl Todos {
    fn add(&mut self, _: &AddTodo, _window: &mut Window, cx: &mut Context<Self>) {
        let title = self.input.read(cx).as_str().trim().to_string();
        if title.is_empty() {
            return;
        }
        self.todos.push(Todo { title: title.into(), done: false });
        self.input.update(cx, |input, cx| input.emplace("", cx));
        cx.notify();
    }
}

# impl Render for Todos {
#     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
    .key_context("Todos")
    .on_action(cx.listener(Self::add))
    // ...the rest of the view, as before
#     }
# }
```

When Enter is pressed, GPUI searches the contexts around the focused element for a
binding whose key matches. The text field is inside `Todos`, so
`enter` → `AddTodo` matches. GPUI dispatches `AddTodo` from the focused element
up through its ancestors until a handler takes it: your `add` method.

`add` reads the field's text through its entity, adds a todo, and clears the field
with `emplace`. Reading another entity with
[`read`](Entity::read) and changing it with [`update`](Entity::update) is how
views work with each other's state.

Actions may look like a long way round compared to listening for the Enter key
directly. They pay off as an app grows: the same action can be bound to
different keys, triggered from a menu or a button, rebound by the user, and
listed with its shortcut in a command palette. [Input and
dispatch](crate::_documentation::concepts::input_dispatch) explains how GPUI
routes keys and actions.

## The program so far

<details>
<summary>src/main.rs</summary>

```standalone_crate,no_run
use gpui::{
    App, Context, Entity, Focusable, FontWeight, KeyBinding, QuitMode, SharedString, Window,
    WindowOptions, actions, colors::Colors, div, prelude::*, px,
};
use gpui_ce_elements::editable_text::{
    EditableTextState, StringStorage,
    actions::{DEFAULT_INPUT_CONTEXT, default_bindings},
    text_input,
};

actions!(todos, [AddTodo]);

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

struct Todos {
    todos: Vec<Todo>,
    input: Entity<EditableTextState>,
}

impl Todos {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let input = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
        window.focus(&input.focus_handle(cx), cx);
        Self { todos: Vec::new(), input }
    }

    fn add(&mut self, _: &AddTodo, _window: &mut Window, cx: &mut Context<Self>) {
        let title = self.input.read(cx).as_str().trim().to_string();
        if title.is_empty() {
            return;
        }
        self.todos.push(Todo { title: title.into(), done: false });
        self.input.update(cx, |input, cx| input.emplace("", cx));
        cx.notify();
    }

    fn row(&self, index: usize, todo: &Todo, colors: &Colors, cx: &mut Context<Self>) -> impl IntoElement + use<> {
        div()
            .id(index)
            .flex()
            .items_center()
            .gap_3()
            .px_2()
            .py_2()
            .rounded_md()
            .cursor_pointer()
            .hover(|style| style.bg(colors.container))
            .child(
                div()
                    .size_5()
                    .rounded_full()
                    .border_1()
                    .border_color(colors.border)
                    .when(todo.done, |checkbox| checkbox.bg(colors.selected)),
            )
            .child(
                div()
                    .flex_1()
                    .when(todo.done, |title| title.line_through().text_color(colors.disabled))
                    .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();
            }))
    }
}

impl Render for Todos {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);
        let left = self.todos.iter().filter(|todo| !todo.done).count();

        div()
            .key_context("Todos")
            .on_action(cx.listener(Self::add))
            .size_full()
            .flex()
            .justify_center()
            .p_8()
            .bg(colors.background)
            .text_color(colors.text)
            .child(
                div()
                    .w_full()
                    .max_w(px(480.))
                    .flex()
                    .flex_col()
                    .gap_4()
                    .child(div().text_3xl().font_weight(FontWeight::SEMIBOLD).child("Todos"))
                    .child(
                        text_input("new-todo")
                            .state(self.input.downgrade())
                            .placeholder("What needs doing?")
                            .px_3()
                            .py_2()
                            .rounded_lg()
                            .border_1()
                            .border_color(colors.border),
                    )
                    .child(
                        div().flex().flex_col().children(
                            self.todos
                                .iter()
                                .enumerate()
                                .map(|(index, todo)| self.row(index, todo, &colors, cx)),
                        ),
                    )
                    .child(div().text_sm().text_color(colors.disabled).child(format!("{left} left"))),
            )
    }
}

fn main() {
    gpui_platform::application()
        .with_quit_mode(QuitMode::LastWindowClosed)
        .run(|cx: &mut App| {
            cx.bind_keys(default_bindings().as_keybindings(Some(DEFAULT_INPUT_CONTEXT)));
            cx.bind_keys([KeyBinding::new("enter", AddTodo, Some("Todos"))]);

            cx.open_window(WindowOptions::default(), |window, cx| {
                cx.new(|cx| Todos::new(window, cx))
            })
            .unwrap();
            cx.activate(true);
        });
}
```

</details>

The field now supplies the text that Chapter 2 stored and Chapter 3 laid out.
Chapter 5 will reuse the same event boundary when rows gain independent controls.
In the [next chapter](crate::_documentation::tutorial::ch5_components),
deleting and filtering make stable ids necessary, and the row becomes a reusable
component.