Textarea

Textarea is the styled control for ordinary multi-line text. Use Input for a single line and Editor for source code.

Install

#Import

use gpui_kit::component::input::{Textarea, TextareaState};
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Textarea } from "textarea.slint";
use iced::widget::text_editor::{Action, Content};
use iced::widget::{column, container, mouse_area, row, scrollable, svg, text};
use iced::{Alignment, Border, Color, Element, Length, Task, Theme};

#Basic usage

let notes = cx.new(|cx| {
    TextareaState::new(window, cx)
        .rows(5)
        .placeholder("Notes")
});

Textarea::new(&notes)
Textarea {
    placeholder: "Tell us about yourself…";
    text <=> root.bio;
}
let basic = column![
    Textarea::new(&app.bio)
        .id("bio")
        .placeholder("Tell us about yourself…")
        .on_action(Message::Bio),
]
.spacing(12.0);

#Auto-grow

let message = cx.new(|cx| {
    TextareaState::new(window, cx)
        .auto_grow(2, 8)
        .placeholder("Write a message")
});

Textarea::new(&message)
VerticalLayout {
    spacing: 12px;
    Textarea {
        height: 200px;
        placeholder: "Release notes…";
        text <=> root.notes;
    }
}
// `Length::Shrink` measures the content, so this field really grows; the
// Slint component cannot measure itself and draws a fixed taller box. Past
// the maximum it scrolls, which is the textarea's counterpart of max_rows.
let auto_grow = column![
    Textarea::new(&app.notes)
        .placeholder("Release notes…")
        .auto_grow(2, 8)
        .on_action(Message::Notes),
]
.spacing(12.0);

The control grows until max_rows; overflowing content then scrolls.

#Value and events

let value = notes.read(cx).value();

notes.update(cx, |state, cx| {
    state.set_value("Updated notes", window, cx);
});

cx.subscribe(&notes, |this, state, event: &InputEvent, cx| {
    if matches!(event, InputEvent::Change) {
        this.notes = state.read(cx).value();
        cx.notify();
    }
});
VerticalLayout {
    spacing: 8px;
    alignment: start;
    Textarea {
        placeholder: "Type a note…";
        edited(text) => {
            root.draft = text;
        }
    }
    Text {
        text: "Draft: " + (root.draft == "" ? "(empty)" : root.draft);
        color: Theme.muted-foreground;
        font-size: Theme.font-size-xs;
        wrap: word-wrap;
    }
}
// The caller owns the `Content`: `on_action` reports every interaction, the
// caller applies it with `perform`, and the text is read back from the same
// value. Reading is `content.text()`, setting is a fresh `Content`.
let events = column![
    Textarea::new(&app.draft)
        .placeholder("Type a note…")
        .rows(4)
        .on_action(Message::Draft),
    muted_line(&format!(
        "Draft: {}",
        if app.draft.is_empty() {
            "(empty)".to_string()
        } else {
            app.draft.text()
        }
    )),
    muted_line(&format!(
        "Last event: {}",
        app.last_event.unwrap_or("nothing yet")
    )),
]
.spacing(8.0);

insert, replace, cursor_position, soft_wrap, searchable, and submit_on_enter are available on TextareaState.

#Sizes

Textarea::new(&notes).large()
Textarea::new(&notes) // medium (default)
Textarea::new(&notes).small()
VerticalLayout {
    spacing: 12px;
    Textarea {
        size: ControlSize.small;
        placeholder: "Small";
    }
    Textarea {
        size: ControlSize.medium;
        placeholder: "Medium";
    }
    Textarea {
        size: ControlSize.large;
        placeholder: "Large";
    }
}
// The size steps the text size and the padding together; each one stands
// three of that size's control heights tall unless `rows` or `auto_grow`
// says otherwise.
let sizes = column![
    Textarea::new(&app.small)
        .size(ControlSize::Small)
        .placeholder("Small")
        .on_action(Message::Small),
    Textarea::new(&app.medium)
        .size(ControlSize::Medium)
        .placeholder("Medium")
        .on_action(Message::Medium),
    Textarea::new(&app.large)
        .size(ControlSize::Large)
        .placeholder("Large")
        .on_action(Message::Large),
]
.spacing(12.0);

The size changes the text size and the padding around the text together. It does not set the height: use h for a fixed height, and let rows or auto_grow decide the height of a growing textarea.

#Appearance

Textarea::new(&notes)
    .h(px(160.))
    .bordered(true)
    .disabled(false)
    .readonly(false)
    .aria_label("Notes")
VerticalLayout {
    spacing: 12px;
    Textarea {
        placeholder: "Normal";
    }
    Textarea {
        placeholder: "Read only";
        read-only: true;
        text: "Generated from the manifest.";
    }
    Textarea {
        placeholder: "Invalid";
        invalid: true;
        text <=> root.snippet;
    }
    Textarea {
        placeholder: "Disabled";
        enabled: false;
    }
}
// The four states the Slint component draws. Read-only swallows the key
// bindings that edit and keeps the frame, the caret and the selection;
// disabled is the absence of a message, which is how this crate spells a
// control that cannot be used.
let appearance = column![
    Textarea::new(&app.normal)
        .placeholder("Normal")
        .on_action(Message::Normal),
    Textarea::new(&app.readonly)
        .readonly(true)
        .on_action(Message::Readonly),
    Textarea::new(&app.invalid)
        .invalid(true)
        .on_action(Message::Invalid),
    Textarea::new(&app.disabled).placeholder("Disabled"),
]
.spacing(12.0);

Unlike disabled, a read-only textarea keeps the normal appearance and still can be focused, selected and copied, it only rejects the changes made by the user.

Textarea deliberately does not expose Input-only adornments such as prefix, suffix, mask toggle, or the clear button. Compose related actions beside the textarea.

#Atomic inline tokens

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// iced's `Content` is one plain string, so a reference cannot live inside it
// as a token that the caret and deletion move as one. This is the Slint
// composer instead: chips on their own line above a borderless field, and
// the text and the references reported apart.
let composer = container(
    column![
        row(REFERENCES.iter().copied().map(token_chip))
            .spacing(6.0)
            .align_y(Alignment::Center),
        Textarea::new(&app.message)
            .appearance(false)
            .placeholder("Write a message…")
            .height(Length::Fill)
            .on_action(Message::Message),
    ]
    .spacing(8.0),
)
.padding(8.0)
.width(Length::Fill)
.height(Length::Fixed(150.0))
.style(|theme: &Theme| {
    let tokens = Tokens::of(theme);
    container::Style {
        background: Some(tokens.background.into()),
        border: Border {
            color: tokens.input,
            width: 1.0,
            radius: theme::RADIUS_MD.into(),
        },
        ..container::Style::default()
    }
});

let tokens = column![
    composer,
    muted_line(&format!(
        "Text: {}",
        if app.message.is_empty() {
            "(empty)".to_string()
        } else {
            app.message.text()
        }
    )),
    muted_line(&format!("References: {}", REFERENCES.join(" · "))),
]
.spacing(10.0);

Use tokens to include mentions, file references or commands in a multi-line message. Insert a token with TextareaState::replace_with_token, or restore a saved draft with set_value. The default label is ready to use; add a renderer when you want an icon or other custom content:

Textarea::new(&state)
    .token(|token, _, _| InputToken::new(token).icon(IconName::File))
VerticalLayout {
    spacing: 10px;
    alignment: start;
    // The frame is the composer's own, which is why the field inside it
    // turns its appearance off.
    Rectangle {
        width: 100%;
        height: 150px;
        border-width: 1px;
        border-color: Theme.input;
        border-radius: Theme.radius-md;
        background: Theme.background;
        VerticalLayout {
            padding: 8px;
            spacing: 8px;
            HorizontalLayout {
                spacing: 6px;
                alignment: start;
                for token[
                    i
                ]
                in root.references: Rectangle {
                    border-radius: Theme.radius-sm;
                    background: Theme.secondary;
                    HorizontalLayout {
                        padding-left: 6px;
                        padding-right: 6px;
                        padding-top: 3px;
                        padding-bottom: 3px;
                        spacing: 4px;
                        cross-axis-alignment: center;
                        Icon {
                            name: "file";
                            size: 12px;
                            color: Theme.muted-foreground;
                        }
                        Text {
                            text: token;
                            color: Theme.foreground;
                            font-size: Theme.font-size-xs;
                            vertical-alignment: center;
                        }
                    }
                }
            }
            Textarea {
                appearance: false;
                horizontal-stretch: 1;
                placeholder: "Write a message…";
                text <=> root.message;
            }
        }
    }
    Text {
        text: "Text: " + (root.message == "" ? "(empty)" : root.message);
        color: Theme.muted-foreground;
        font-size: Theme.font-size-xs;
        wrap: word-wrap;
    }
    Text {
        text: "References: src/main.rs · tests/dock.rs · Cargo.toml";
        color: Theme.muted-foreground;
        font-size: Theme.font-size-xs;
        wrap: word-wrap;
    }
}

Import InputToken from gpui_kit::component::input and IconName from gpui_kit::component. A token wraps onto the next line as a whole, and auto-grow adjusts the textarea height to fit. Put line breaks in the text between tokens. Use on_token_hover to show a tooltip or preview without selecting or editing; disabled tokens never report hover entry; disabling a hovered token sends its exit. See Input: atomic inline tokens for editing, activation, draft persistence, mode restrictions and JavaScript APIs.