## Screenshot From `input_story`: <img width="1069" height="122" alt="Screenshot From 2025-11-28 09-14-03" src="https://github.com/user-attachments/assets/f14ef2fc-4939-4ceb-be3e-bede492f8602" /> ## Breaking Changes - `.multi_line()` was changed to `.multi_line(bool)`. - Removed pub `InputMode`, this should only for internal. ```diff InputState::new(window, cx) - .multi_line() + .multi_line(true) ``` ## Checklist - [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and followed the guidelines. - [x] Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate. - [x] Passed `cargo run` for story tests related to the changes. - [ ] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific) Use cases, 1) In my API client, I have a tree sitter language for the URL input, <img width="1001" height="162" alt="Image" src="https://github.com/user-attachments/assets/9771decf-b0b7-4230-8ed6-784a95b72af1" /> 2) In my SQL editor I want to be able to allow JSON syntax highlighting for inline editing of json columns, <img width="501" height="130" alt="Image" src="https://github.com/user-attachments/assets/f0f879cc-841f-49d2-a23a-2effc3747f38" /> --------- Co-authored-by: Jason Lee <huacnlee@gmail.com>
9.4 KiB
| title | description |
|---|---|
| Editor | Multi-line text input component with auto-resize, validation, and advanced editing features. |
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
use gpui_component::input::{InputState, Input};
Usage
Textarea
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line(true)
.placeholder("Enter your message...")
);
Input::new(&state)
With fixed height Textarea:
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line(true)
.rows(10) // Set number of rows
.placeholder("Enter text here...")
);
Input::new(&state)
.h(px(320.)) // Set explicit height
AutoGrow
let state = cx.new(|cx|
InputState::new(window, cx)
.auto_grow(1, 5) // min_rows: 1, max_rows: 5
.placeholder("Type here and watch it grow...")
);
Input::new(&state)
CodeEditor
GPUI Component's InputState supports a code editor mode with syntax highlighting, line numbers, and search functionality.
It design for high performance and can handle large files efficiently. We used tree-sitter for syntax highlighting, and ropey for text storage and manipulation.
let state = 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}")
);
Input::new(&state)
.h_full() // Full height
Single Line Mode
Sometimes you may want to use the code editor features but restrict input to a single line, for example for code snippets or commands.
let state = cx.new(|cx|
InputState::new(window, cx)
.code_editor("rust")
.multi_line(false) // Single line
.default_value("println!(\"Hello, world!\");")
);
Input::new(&state)
TabSize
use gpui_component::input::TabSize;
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line(true)
.tab_size(TabSize {
tab_size: 4,
hard_tabs: false, // Use spaces instead of tabs
})
);
Input::new(&state)
Searchable
The search feature allows for all multi-line inputs to support searching through the content using Ctrl+F (or Cmd+F on Mac).
It provides a search bar with options to navigate between matches and highlight them.
Use searchable method to enable:
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line(true)
.searchable(true) // Enable Ctrl+F search
.rows(15)
.default_value("Search through this content...")
);
Input::new(&state)
SoftWrap
By default multi-line inputs have soft wrapping enabled, meaning long lines will wrap to fit the width of the textarea.
You can disable soft wrapping to allow horizontal scrolling instead:
// With soft wrap (default)
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line(true)
.soft_wrap(true)
.rows(6)
);
// Without soft wrap (horizontal scrolling)
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line(true)
.soft_wrap(false)
.rows(6)
.default_value("This is a very long line that will not wrap automatically but will show horizontal scrollbar instead.")
);
Text Manipulation
// Insert text at cursor position
state.update(cx, |state, cx| {
state.insert("inserted text", window, cx);
});
// Replace all content
state.update(cx, |state, cx| {
state.replace("new content", window, cx);
});
// Set cursor position
state.update(cx, |state, cx| {
state.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
});
// Get cursor position
let position = state.read(cx).cursor_position();
println!("Line: {}, Column: {}", position.line, position.character);
Validation
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line(true)
.validate(|text, _| {
// Validate that content is not empty and under 1000 chars
!text.trim().is_empty() && text.len() <= 1000
})
);
Input::new(&state)
Handle Events
cx.subscribe_in(&state, 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
Input::new(&state)
.disabled(true)
.h(px(200.))
Custom Styling
// Without default appearance
Input::new(&state)
.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(
Input::new(&state)
.appearance(false)
.h(px(150.))
)
Examples
Comment Box
struct CommentBox {
state: Entity<InputState>,
char_limit: usize,
}
impl CommentBox {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let state = cx.new(|cx|
InputState::new(window, cx)
.auto_grow(3, 8)
.placeholder("Write your comment...")
.validate(|text, _| text.len() <= 500)
);
Self {
state,
char_limit: 500,
}
}
}
impl Render for CommentBox {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let content = self.state.read(cx).value();
let char_count = content.len();
let remaining = self.char_limit.saturating_sub(char_count);
v_flex()
.gap_2()
.child(Input::new(&self.state))
.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
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(
Input::new(&self.editor)
.h(px(400.))
.bordered(true)
)
}
}
Text Editor with Toolbar
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(
Input::new(&self.editor)
.h(px(300.))
)
}
}