bezel

Components / Selection & input

Textarea

crates/ui/src/input.rs

A textarea is a TextField with a different Shape. Editing is identical across all three shapes — every action works on the content and a byte range, and none of them cares where the lines break. What the shape decides is the box:

use ui::input::{Shape, TextField};

TextField::new(cx).with_shape(Shape::Rows(4))
TextField::new(cx).with_shape(Shape::Grow { min: 3, max: 12 })

Rows(n) is exactly n lines tall and scrolls past that. Grow { min, max } is the composer shape: it grows with the content and scrolls once it hits max. Shape::Line is the single-line default, where a pasted newline becomes a space rather than silently truncating what was pasted.

Multi-line fields claim a second key context, MULTILINE_KEY_CONTEXT, and enter, up, down and their shift variants are bound there rather than on every field. A single-line field is routinely nested inside something that has already claimed those keys — the command palette and the combobox both drive their lists with up/down/enter, and their query field sits deeper in the focus path, so a binding on every field would win the dispatch and break list navigation in both.

When enter needs to mean something else in one particular box, give that box a context of its own:

const COMPOSER: &str = "Composer";

cx.bind_keys([
    KeyBinding::new("enter", Send, Some(COMPOSER)),
    KeyBinding::new("shift-enter", input::InsertNewline, Some(COMPOSER)),
]);

let field = cx.new(|cx| {
    TextField::new(cx)
        .with_shape(Shape::Grow { min: 3, max: 12 })
        .with_key_context(COMPOSER)
});

gpui resolves a keystroke to the binding whose context matches deepest in the focus path, and nothing is deeper than the focused field — so a container around it cannot win enter, however it is bound. Rebinding the shared multi-line context would win, and would take the newline away from every other textarea in the app.

home/ctrl-a goes to the start of the logical line — the byte after the previous newline — not to the start of the visual row a soft wrap put you on. That is emacs' C-a, and a deliberate divergence from NSTextView.

Textarea