Skip to content

Editor

A powerful multi-line text input component that extends the basic input functionality with support for multiple lines, auto-resizing, syntax highlighting, line numbers, and code editing features. Perfect for forms, code editors, and content editing.

Import

rust
use gpui_component::input::{InputState, TextInput};

Usage

Basic Textarea

rust
let textarea = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line()
        .placeholder("Enter your message...")
);

TextInput::new(&textarea)

Fixed Height Textarea

rust
let textarea = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line()
        .rows(10) // Set number of rows
        .placeholder("Enter text here...")
);

TextInput::new(&textarea)
    .h(px(320.)) // Set explicit height

Auto-Resizing Textarea

rust
let textarea = cx.new(|cx|
    InputState::new(window, cx)
        .auto_grow(1, 5) // min_rows: 1, max_rows: 5
        .placeholder("Type here and watch it grow...")
);

TextInput::new(&textarea)

With Default Content

rust
let textarea = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line()
        .rows(6)
        .default_value("Hello World!\n\nThis is a multi-line textarea with default content.")
);

TextInput::new(&textarea)

Code Editor Mode

rust
let code_editor = cx.new(|cx|
    InputState::new(window, cx)
        .code_editor("rust") // Language for syntax highlighting
        .line_number(true) // Show line numbers
        .searchable(true) // Enable search functionality
        .default_value("fn main() {\n    println!(\"Hello, world!\");\n}")
);

TextInput::new(&code_editor)
    .h_full() // Full height

Textarea with Custom Tab Size

rust
use gpui_component::input::TabSize;

let textarea = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line()
        .tab_size(TabSize {
            tab_size: 4,
            hard_tabs: false, // Use spaces instead of tabs
        })
);

TextInput::new(&textarea)

Searchable Textarea

rust
let textarea = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line()
        .searchable(true) // Enable Ctrl+F search
        .rows(15)
        .default_value("Search through this content...")
);

TextInput::new(&textarea)

Soft Wrap Control

rust
// With soft wrap (default)
let textarea_wrap = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line()
        .soft_wrap(true)
        .rows(6)
);

// Without soft wrap (horizontal scrolling)
let textarea_no_wrap = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line()
        .soft_wrap(false)
        .rows(6)
        .default_value("This is a very long line that will not wrap automatically but will show horizontal scrollbar instead.")
);

v_flex()
    .gap_4()
    .child(TextInput::new(&textarea_wrap))
    .child(TextInput::new(&textarea_no_wrap))

Character Counting

rust
struct TextareaView {
    textarea: Entity<InputState>,
}

impl Render for TextareaView {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let char_count = self.textarea.read(cx).value().len();
        let max_chars = 500;

        v_flex()
            .gap_2()
            .child(
                TextInput::new(&self.textarea)
                    .h(px(120.))
            )
            .child(
                div()
                    .text_right()
                    .text_sm()
                    .text_color(if char_count > max_chars {
                        cx.theme().destructive
                    } else {
                        cx.theme().muted_foreground
                    })
                    .child(format!("{}/{}", char_count, max_chars))
            )
    }
}

Text Manipulation

rust
// Insert text at cursor position
textarea.update(cx, |input, cx| {
    input.insert("inserted text", window, cx);
});

// Replace all content
textarea.update(cx, |input, cx| {
    input.replace("new content", window, cx);
});

// Set cursor position
textarea.update(cx, |input, cx| {
    input.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
});

// Get cursor position
let position = textarea.read(cx).cursor_position();
println!("Line: {}, Column: {}", position.line, position.character);

Validation

rust
let textarea = cx.new(|cx|
    InputState::new(window, cx)
        .multi_line()
        .validate(|text, _| {
            // Validate that content is not empty and under 1000 chars
            !text.trim().is_empty() && text.len() <= 1000
        })
);

TextInput::new(&textarea)

Handle Events

rust
cx.subscribe_in(&textarea, window, |view, state, event, window, cx| {
    match event {
        InputEvent::Change => {
            let content = state.read(cx).value();
            println!("Content changed: {} characters", content.len());
        }
        InputEvent::PressEnter { secondary } => {
            if secondary {
                println!("Shift+Enter pressed - insert line break");
            } else {
                println!("Enter pressed - could submit form");
            }
        }
        InputEvent::Focus => println!("Textarea focused"),
        InputEvent::Blur => println!("Textarea blurred"),
    }
});

Disabled State

rust
TextInput::new(&textarea)
    .disabled(true)
    .h(px(200.))

Custom Styling

rust
// Without default appearance
TextInput::new(&textarea)
    .appearance(false)
    .h(px(200.))

// Custom container styling
div()
    .bg(cx.theme().background)
    .border_2()
    .border_color(cx.theme().input)
    .rounded_lg()
    .p_4()
    .child(
        TextInput::new(&textarea)
            .appearance(false)
            .h(px(150.))
    )

API Reference

InputState (Multi-line Methods)

MethodDescription
multi_line()Enable multi-line mode with 2 rows default
auto_grow(min, max)Enable auto-resize between min and max rows
code_editor(language)Enable code editor mode with syntax highlighting
rows(count)Set number of visible rows
tab_size(TabSize)Configure tab behavior
searchable(bool)Enable/disable search (Ctrl+F)
soft_wrap(bool)Enable/disable text wrapping
line_number(bool)Show/hide line numbers (code editor only)
cursor_position()Get current cursor position as Position
set_cursor_position(pos, window, cx)Set cursor to specific line/column
insert(text, window, cx)Insert text at cursor
replace(text, window, cx)Replace all content

TextInput (Multi-line Methods)

MethodDescription
h(height)Set explicit height
h_full()Take full available height

Position

FieldDescription
line0-based line number
character0-based character position in line

TabSize

FieldDescription
tab_sizeNumber of spaces per tab (default: 2)
hard_tabsUse actual tab characters (default: false)

Keyboard Shortcuts

ShortcutAction
EnterInsert new line
Shift+EnterInsert new line (secondary)
TabIndent line/selection
Shift+TabOutdent line/selection
Ctrl/Cmd+ASelect all
Ctrl/Cmd+ZUndo
Ctrl/Cmd+YRedo
Ctrl/Cmd+FOpen search (if enabled)
Ctrl/Cmd+[Outdent block
Ctrl/Cmd+]Indent block

Examples

Comment Box

rust
struct CommentBox {
    textarea: Entity<InputState>,
    char_limit: usize,
}

impl CommentBox {
    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
        let textarea = cx.new(|cx|
            InputState::new(window, cx)
                .auto_grow(3, 8)
                .placeholder("Write your comment...")
                .validate(|text, _| text.len() <= 500)
        );

        Self {
            textarea,
            char_limit: 500,
        }
    }
}

impl Render for CommentBox {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let content = self.textarea.read(cx).value();
        let char_count = content.len();
        let remaining = self.char_limit.saturating_sub(char_count);

        v_flex()
            .gap_2()
            .child(TextInput::new(&self.textarea))
            .child(
                h_flex()
                    .justify_between()
                    .child(
                        div()
                            .text_xs()
                            .text_color(cx.theme().muted_foreground)
                            .child(format!("{} characters remaining", remaining))
                    )
                    .child(
                        Button::new("submit")
                            .primary()
                            .disabled(char_count == 0 || char_count > self.char_limit)
                            .label("Post Comment")
                    )
            )
    }
}

Code Editor with Language Selection

rust
struct CodeEditor {
    editor: Entity<InputState>,
    language: String,
}

impl CodeEditor {
    fn set_language(&mut self, language: String, window: &mut Window, cx: &mut Context<Self>) {
        self.language = language.clone();
        self.editor.update(cx, |editor, cx| {
            editor.set_highlighter(language, cx);
        });
    }
}

impl Render for CodeEditor {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_3()
            .child(
                h_flex()
                    .gap_2()
                    .child("Language:")
                    .child(
                        // Language selector dropdown would go here
                        div().child(self.language.clone())
                    )
            )
            .child(
                TextInput::new(&self.editor)
                    .h(px(400.))
                    .bordered(true)
            )
    }
}

Text Editor with Toolbar

rust
struct TextEditor {
    editor: Entity<InputState>,
}

impl TextEditor {
    fn format_bold(&mut self, window: &mut Window, cx: &mut Context<Self>) {
        self.editor.update(cx, |editor, cx| {
            if !editor.selected_range.is_empty() {
                let selected = editor.selected_text().to_string();
                editor.replace(&format!("**{}**", selected), window, cx);
            }
        });
    }
}

impl Render for TextEditor {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        v_flex()
            .gap_2()
            .child(
                h_flex()
                    .gap_1()
                    .p_2()
                    .border_b_1()
                    .border_color(cx.theme().border)
                    .child(
                        Button::new("bold")
                            .ghost()
                            .icon(IconName::Bold)
                            .on_click(cx.listener(Self::format_bold))
                    )
                    .child(
                        Button::new("italic")
                            .ghost()
                            .icon(IconName::Italic)
                    )
            )
            .child(
                TextInput::new(&self.editor)
                    .h(px(300.))
            )
    }
}

Accessibility

  • Full keyboard navigation support
  • Screen reader compatibility
  • Clear focus indicators
  • Proper ARIA labels and roles
  • Support for assistive text selection
  • Keyboard shortcuts follow platform conventions
  • ESC key handling for search and context menus
  • Tab key properly indents in multi-line mode

Performance Notes

  • Optimized for large text content (up to 50K lines in code editor mode)
  • Efficient text wrapping and line measurement
  • Virtual scrolling for long documents
  • Minimal re-renders on text changes
  • Efficient syntax highlighting with tree-sitter
  • Smart auto-grow calculations

Best Practices

  1. Auto-resize: Use auto_grow() for dynamic content like comments or messages
  2. Fixed size: Use multi_line().rows(n) for consistent layouts like forms
  3. Code editing: Use code_editor() for syntax-aware editing
  4. Validation: Always validate long-form content on the client side
  5. Character limits: Show character counters for user guidance
  6. Search: Enable search for long content areas
  7. Soft wrap: Disable for code, enable for prose