input: Add to support CodeEditor as single line mode. (#1696)

## 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>
This commit is contained in:
Andreas Johansson 2025-11-28 12:51:40 +01:00 committed by GitHub
parent 1ce09cc1d1
commit 7e479aa7f2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 302 additions and 240 deletions

View file

@ -22,7 +22,7 @@ impl Example {
let editor = cx.new(|cx| {
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.tab_size(TabSize {
tab_size: 4,
hard_tabs: false,

View file

@ -23,6 +23,7 @@ pub struct InputStory {
mask_input2: Entity<InputState>,
currency_input: Entity<InputState>,
custom_input: Entity<InputState>,
code_input: Entity<InputState>,
_subscriptions: Vec<Subscription>,
}
@ -87,6 +88,13 @@ impl InputStory {
let custom_input =
cx.new(|cx| InputState::new(window, cx).placeholder("here is a custom input"));
let code_input = cx.new(|cx| {
InputState::new(window, cx)
.code_editor("json")
.multi_line(false)
.default_value(r#"{"single_line":"code editor"}"#)
});
let _subscriptions = vec![
cx.subscribe_in(&input1, window, Self::on_input_event),
cx.subscribe_in(&input2, window, Self::on_input_event),
@ -113,6 +121,7 @@ impl InputStory {
mask_input2,
currency_input,
custom_input,
code_input,
_subscriptions,
}
}
@ -250,5 +259,10 @@ impl Render for InputStory {
.child(Input::new(&self.custom_input).appearance(false)),
),
)
.child(
section("Single line code editor")
.max_w_md()
.child(Input::new(&self.code_input)),
)
}
}

View file

@ -47,7 +47,7 @@ impl TextareaStory {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let textarea = cx.new(|cx| {
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.rows(10)
.placeholder("Enter text here...")
.searchable(true)
@ -77,7 +77,7 @@ impl TextareaStory {
let textarea_no_wrap = cx.new(|cx| {
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.rows(6)
.soft_wrap(false)
.default_value("This is a very long line of text to test if the horizontal scrolling function is working properly, and it should not wrap automatically but display a horizontal scrollbar.\nThe second line is also very long text, used to test the horizontal scrolling effect under multiple lines, and you can input more content to test.\nThe third line: Here you can input other long text content that requires horizontal scrolling.\n")

View file

@ -567,12 +567,12 @@ impl TextElement {
bg_segments: &[(Range<usize>, Hsla)],
window: &mut Window,
) -> Vec<LineLayout> {
let is_multi_line = state.mode.is_multi_line();
let is_single_line = state.mode.is_single_line();
let text_wrapper = &state.text_wrapper;
let visible_range = &last_layout.visible_range;
let visible_range_offset = &last_layout.visible_range_offset;
if !is_multi_line {
if is_single_line {
let shaped_line = window.text_system().shape_line(
display_text.to_string().into(),
font_size,
@ -657,6 +657,7 @@ impl TextElement {
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
let state = self.state.read(cx);
let text = &state.text;
let is_multi_line = state.mode.is_multi_line();
let (highlighter, diagnostics) = match &state.mode {
InputMode::CodeEditor {
@ -676,8 +677,13 @@ impl TextElement {
.skip(visible_range.start)
.take(visible_range.len())
{
// +1 for `\n`
let line_len = line.len() + 1;
let line_len = if is_multi_line {
// +1 for `\n`
line.len() + 1
} else {
line.len()
};
let range = offset..offset + line_len;
let line_styles = highlighter.styles(&range, &cx.theme().highlight_theme);
styles = gpui::combine_highlights(styles, line_styles).collect();

View file

@ -1,15 +1,15 @@
use gpui::{
point, px, Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels,
SharedString, TextRun, TextStyle, Window,
Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
TextRun, TextStyle, Window, point, px,
};
use ropey::RopeSlice;
use crate::{
input::{
element::TextElement, mode::InputMode, Indent, IndentInline, InputState, LastLayout,
Outdent, OutdentInline,
},
RopeExt,
input::{
Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, element::TextElement,
mode::InputMode,
},
};
#[derive(Debug, Copy, Clone)]
@ -56,16 +56,22 @@ impl TabSize {
impl InputMode {
#[inline]
pub(super) fn is_indentable(&self) -> bool {
matches!(
self,
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. }
)
match self {
InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => {
*multi_line
}
_ => false,
}
}
#[inline]
pub(super) fn has_indent_guides(&self) -> bool {
match self {
InputMode::CodeEditor { indent_guides, .. } => *indent_guides,
InputMode::CodeEditor {
indent_guides,
multi_line,
..
} => *indent_guides && *multi_line,
_ => false,
}
}
@ -73,7 +79,7 @@ impl InputMode {
#[inline]
pub(super) fn tab_size(&self) -> TabSize {
match self {
InputMode::MultiLine { tab, .. } => *tab,
InputMode::PlainText { tab, .. } => *tab,
InputMode::CodeEditor { tab, .. } => *tab,
_ => TabSize::default(),
}
@ -168,7 +174,7 @@ impl InputState {
///
/// Only for [`InputMode::CodeEditor`] mode.
pub fn indent_guides(mut self, indent_guides: bool) -> Self {
debug_assert!(self.mode.is_code_editor());
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
if let InputMode::CodeEditor {
indent_guides: l, ..
} = &mut self.mode
@ -199,11 +205,11 @@ impl InputState {
/// Set the tab size for the input.
///
/// Only for [`InputMode::MultiLine`] and [`InputMode::CodeEditor`] mode.
/// Only for [`InputMode::PlainText`] and [`InputMode::CodeEditor`] mode with multi_line.
pub fn tab_size(mut self, tab: TabSize) -> Self {
debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
match &mut self.mode {
InputMode::MultiLine { tab: t, .. } => *t = tab,
InputMode::PlainText { tab: t, .. } => *t = tab,
InputMode::CodeEditor { tab: t, .. } => *t = tab,
_ => {}
}

View file

@ -10,20 +10,23 @@ use crate::highlighter::DiagnosticSet;
use crate::highlighter::SyntaxHighlighter;
use crate::input::{RopeExt as _, TabSize};
#[derive(Default, Clone)]
pub enum InputMode {
#[default]
SingleLine,
MultiLine {
#[derive(Clone)]
pub(crate) enum InputMode {
/// A plain text input mode.
PlainText {
multi_line: bool,
tab: TabSize,
rows: usize,
},
/// An auto grow input mode.
AutoGrow {
rows: usize,
min_rows: usize,
max_rows: usize,
},
/// A code editor input mode.
CodeEditor {
multi_line: bool,
tab: TabSize,
rows: usize,
/// Show line number
@ -35,11 +38,58 @@ pub enum InputMode {
},
}
impl Default for InputMode {
fn default() -> Self {
InputMode::plain_text()
}
}
#[allow(unused)]
impl InputMode {
/// Create a plain input mode with default settings.
pub(super) fn plain_text() -> Self {
InputMode::PlainText {
multi_line: false,
tab: TabSize::default(),
rows: 1,
}
}
/// Create a code editor input mode with default settings.
pub(super) fn code_editor(language: impl Into<SharedString>) -> Self {
InputMode::CodeEditor {
rows: 2,
multi_line: true,
tab: TabSize::default(),
language: language.into(),
highlighter: Rc::new(RefCell::new(None)),
line_number: true,
indent_guides: true,
diagnostics: DiagnosticSet::new(&Rope::new()),
}
}
/// Create an auto grow input mode with given min and max rows.
pub(super) fn auto_grow(min_rows: usize, max_rows: usize) -> Self {
InputMode::AutoGrow {
rows: min_rows,
min_rows,
max_rows,
}
}
pub(super) fn multi_line(mut self, multi_line: bool) -> Self {
match &mut self {
InputMode::PlainText { multi_line: ml, .. } => *ml = multi_line,
InputMode::CodeEditor { multi_line: ml, .. } => *ml = multi_line,
InputMode::AutoGrow { .. } => {}
}
self
}
#[inline]
pub(super) fn is_single_line(&self) -> bool {
matches!(self, InputMode::SingleLine)
!self.is_multi_line()
}
#[inline]
@ -54,15 +104,16 @@ impl InputMode {
#[inline]
pub(super) fn is_multi_line(&self) -> bool {
matches!(
self,
InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } | InputMode::CodeEditor { .. }
)
match self {
InputMode::PlainText { multi_line, .. } => *multi_line,
InputMode::CodeEditor { multi_line, .. } => *multi_line,
InputMode::AutoGrow { max_rows, .. } => *max_rows > 1,
}
}
pub(super) fn set_rows(&mut self, new_rows: usize) {
match self {
InputMode::MultiLine { rows, .. } => {
InputMode::PlainText { rows, .. } => {
*rows = new_rows;
}
InputMode::CodeEditor { rows, .. } => {
@ -75,7 +126,6 @@ impl InputMode {
} => {
*rows = new_rows.clamp(*min_rows, *max_rows);
}
_ => {}
}
}
@ -90,11 +140,14 @@ impl InputMode {
/// At least 1 row be return.
pub(super) fn rows(&self) -> usize {
if !self.is_multi_line() {
return 1;
}
match self {
InputMode::MultiLine { rows, .. } => *rows,
InputMode::PlainText { rows, .. } => *rows,
InputMode::CodeEditor { rows, .. } => *rows,
InputMode::AutoGrow { rows, .. } => *rows,
_ => 1,
}
.max(1)
}
@ -103,7 +156,6 @@ impl InputMode {
#[allow(unused)]
pub(super) fn min_rows(&self) -> usize {
match self {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => 1,
InputMode::AutoGrow { min_rows, .. } => *min_rows,
_ => 1,
}
@ -112,10 +164,13 @@ impl InputMode {
#[allow(unused)]
pub(super) fn max_rows(&self) -> usize {
if !self.is_multi_line() {
return 1;
}
match self {
InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => usize::MAX,
InputMode::AutoGrow { max_rows, .. } => *max_rows,
_ => 1,
_ => usize::MAX,
}
}
@ -124,7 +179,11 @@ impl InputMode {
#[inline]
pub(super) fn line_number(&self) -> bool {
match self {
InputMode::CodeEditor { line_number, .. } => *line_number,
InputMode::CodeEditor {
line_number,
multi_line,
..
} => *line_number && *multi_line,
_ => false,
}
}
@ -202,3 +261,88 @@ impl InputMode {
}
}
}
#[cfg(test)]
mod tests {
use ropey::Rope;
use crate::{
highlighter::DiagnosticSet,
input::{TabSize, mode::InputMode},
};
#[test]
fn test_code_editor() {
let mode = InputMode::code_editor("rust");
assert_eq!(mode.is_code_editor(), true);
assert_eq!(mode.is_multi_line(), true);
assert_eq!(mode.is_single_line(), false);
assert_eq!(mode.line_number(), true);
assert_eq!(mode.has_indent_guides(), true);
assert_eq!(mode.max_rows(), usize::MAX);
assert_eq!(mode.min_rows(), 1);
let mode = InputMode::CodeEditor {
multi_line: false,
line_number: true,
indent_guides: true,
rows: 0,
tab: Default::default(),
language: "rust".into(),
highlighter: Default::default(),
diagnostics: DiagnosticSet::new(&Rope::new()),
};
assert_eq!(mode.is_code_editor(), true);
assert_eq!(mode.is_multi_line(), false);
assert_eq!(mode.is_single_line(), true);
assert_eq!(mode.line_number(), false);
assert_eq!(mode.has_indent_guides(), false);
assert_eq!(mode.max_rows(), 1);
assert_eq!(mode.min_rows(), 1);
}
#[test]
fn test_plain() {
let mode = InputMode::PlainText {
multi_line: true,
tab: TabSize::default(),
rows: 5,
};
assert_eq!(mode.is_code_editor(), false);
assert_eq!(mode.is_multi_line(), true);
assert_eq!(mode.is_single_line(), false);
assert_eq!(mode.line_number(), false);
assert_eq!(mode.rows(), 5);
assert_eq!(mode.max_rows(), usize::MAX);
assert_eq!(mode.min_rows(), 1);
let mode = InputMode::plain_text();
assert_eq!(mode.is_code_editor(), false);
assert_eq!(mode.is_multi_line(), false);
assert_eq!(mode.is_single_line(), true);
assert_eq!(mode.line_number(), false);
assert_eq!(mode.max_rows(), 1);
assert_eq!(mode.min_rows(), 1);
}
#[test]
fn test_auto_grow() {
let mut mode = InputMode::auto_grow(2, 5);
assert_eq!(mode.is_code_editor(), false);
assert_eq!(mode.is_multi_line(), true);
assert_eq!(mode.is_single_line(), false);
assert_eq!(mode.line_number(), false);
assert_eq!(mode.rows(), 2);
assert_eq!(mode.max_rows(), 5);
assert_eq!(mode.min_rows(), 2);
mode.set_rows(4);
assert_eq!(mode.rows(), 4);
mode.set_rows(1);
assert_eq!(mode.rows(), 2);
mode.set_rows(10);
assert_eq!(mode.rows(), 5);
}
}

View file

@ -12,15 +12,14 @@ use gpui::{
};
use ropey::{Rope, RopeSlice};
use serde::Deserialize;
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
use sum_tree::Bias;
use unicode_segmentation::*;
use super::{
TabSize, blink_cursor::BlinkCursor, change::Change, element::TextElement,
mask_pattern::MaskPattern, mode::InputMode, number_input, text_wrapper::TextWrapper,
blink_cursor::BlinkCursor, change::Change, element::TextElement, mask_pattern::MaskPattern,
mode::InputMode, number_input, text_wrapper::TextWrapper,
};
use crate::Size;
use crate::actions::{SelectDown, SelectLeft, SelectRight, SelectUp};
@ -391,7 +390,7 @@ impl InputState {
loading: false,
pattern: None,
validate: None,
mode: InputMode::SingleLine,
mode: InputMode::default(),
last_layout: None,
last_bounds: None,
last_selected_range: None,
@ -417,24 +416,17 @@ impl InputState {
}
}
/// Set Input to use [`InputMode::MultiLine`] mode.
/// Set Input to use multi line mode.
///
/// Default rows is 2.
pub fn multi_line(mut self) -> Self {
self.mode = InputMode::MultiLine {
rows: 2,
tab: TabSize::default(),
};
pub fn multi_line(mut self, multi_line: bool) -> Self {
self.mode = self.mode.multi_line(multi_line);
self
}
/// Set Input to use [`InputMode::AutoGrow`] mode with min, max rows limit.
pub fn auto_grow(mut self, min_rows: usize, max_rows: usize) -> Self {
self.mode = InputMode::AutoGrow {
rows: min_rows,
min_rows: min_rows,
max_rows: max_rows,
};
self.mode = InputMode::auto_grow(min_rows, max_rows);
self
}
@ -445,7 +437,9 @@ impl InputState {
/// - line_number: true
/// - tab_size: 2
/// - hard_tabs: false
/// - height: full
/// - height: 100%
/// - multi_line: true
/// - indent_guides: true
///
/// If `highlighter` is None, will use the default highlighter.
///
@ -459,15 +453,7 @@ impl InputState {
/// - Large Text support, up to 50K lines.
pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self {
let language: SharedString = language.into();
self.mode = InputMode::CodeEditor {
rows: 2,
tab: TabSize::default(),
language,
highlighter: Rc::new(RefCell::new(None)),
line_number: true,
indent_guides: true,
diagnostics: DiagnosticSet::new(&Rope::new()),
};
self.mode = InputMode::code_editor(language);
self.searchable = true;
self
}
@ -487,7 +473,7 @@ impl InputState {
/// Set enable/disable line number, only for [`InputMode::CodeEditor`] mode.
pub fn line_number(mut self, line_number: bool) -> Self {
debug_assert!(self.mode.is_code_editor());
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number;
}
@ -496,7 +482,7 @@ impl InputState {
/// Set line number, only for [`InputMode::CodeEditor`] mode.
pub fn set_line_number(&mut self, line_number: bool, _: &mut Window, cx: &mut Context<Self>) {
debug_assert!(self.mode.is_code_editor());
debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
if let InputMode::CodeEditor { line_number: l, .. } = &mut self.mode {
*l = line_number;
}
@ -510,7 +496,9 @@ impl InputState {
/// default: 2
pub fn rows(mut self, rows: usize) -> Self {
match &mut self.mode {
InputMode::MultiLine { rows: r, .. } => *r = rows,
InputMode::PlainText { rows: r, .. } | InputMode::CodeEditor { rows: r, .. } => {
*r = rows
}
InputMode::AutoGrow {
max_rows: max_r,
rows: r,
@ -519,7 +507,6 @@ impl InputState {
*r = rows;
*max_r = rows;
}
_ => {}
}
self
}

View file

@ -15,61 +15,53 @@ use gpui_component::input::{InputState, Input};
## Usage
### Basic Textarea
### Textarea
```rust
let textarea = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.placeholder("Enter your message...")
);
Input::new(&textarea)
Input::new(&state)
```
### Fixed Height Textarea
With fixed height Textarea:
```rust
let textarea = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.rows(10) // Set number of rows
.placeholder("Enter text here...")
);
Input::new(&textarea)
Input::new(&state)
.h(px(320.)) // Set explicit height
```
### Auto-Resizing Textarea
### AutoGrow
```rust
let textarea = cx.new(|cx|
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(&textarea)
Input::new(&state)
```
### With Default Content
### 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](https://tree-sitter.github.io/tree-sitter/) for syntax highlighting, and [ropey](https://github.com/cessen/ropey) for text storage and manipulation.
```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.")
);
Input::new(&textarea)
```
### Code Editor Mode
```rust
let code_editor = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.code_editor("rust") // Language for syntax highlighting
.line_number(true) // Show line numbers
@ -77,142 +69,129 @@ let code_editor = cx.new(|cx|
.default_value("fn main() {\n println!(\"Hello, world!\");\n}")
);
Input::new(&code_editor)
Input::new(&state)
.h_full() // Full height
```
### Textarea with Custom Tab Size
#### 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.
```rust
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
```rust
use gpui_component::input::TabSize;
let textarea = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.tab_size(TabSize {
tab_size: 4,
hard_tabs: false, // Use spaces instead of tabs
})
);
Input::new(&textarea)
Input::new(&state)
```
### Searchable Textarea
### 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:
```rust
let textarea = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.searchable(true) // Enable Ctrl+F search
.rows(15)
.default_value("Search through this content...")
);
Input::new(&textarea)
Input::new(&state)
```
### Soft Wrap Control
### 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:
```rust
// With soft wrap (default)
let textarea_wrap = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.soft_wrap(true)
.rows(6)
);
// Without soft wrap (horizontal scrolling)
let textarea_no_wrap = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line()
.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.")
);
v_flex()
.gap_4()
.child(Input::new(&textarea_wrap))
.child(Input::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(
Input::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);
state.update(cx, |state, cx| {
state.insert("inserted text", window, cx);
});
// Replace all content
textarea.update(cx, |input, cx| {
input.replace("new content", window, cx);
state.update(cx, |state, cx| {
state.replace("new content", window, cx);
});
// Set cursor position
textarea.update(cx, |input, cx| {
input.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
state.update(cx, |state, cx| {
state.set_cursor_position(Position { line: 2, character: 5 }, window, cx);
});
// Get cursor position
let position = textarea.read(cx).cursor_position();
let position = state.read(cx).cursor_position();
println!("Line: {}, Column: {}", position.line, position.character);
```
### Validation
```rust
let textarea = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.multi_line()
.multi_line(true)
.validate(|text, _| {
// Validate that content is not empty and under 1000 chars
!text.trim().is_empty() && text.len() <= 1000
})
);
Input::new(&textarea)
Input::new(&state)
```
### Handle Events
```rust
cx.subscribe_in(&textarea, window, |view, state, event, window, cx| {
cx.subscribe_in(&state, window, |view, state, event, window, cx| {
match event {
InputEvent::Change => {
let content = state.read(cx).value();
@ -234,7 +213,7 @@ cx.subscribe_in(&textarea, window, |view, state, event, window, cx| {
### Disabled State
```rust
Input::new(&textarea)
Input::new(&state)
.disabled(true)
.h(px(200.))
```
@ -243,7 +222,7 @@ Input::new(&textarea)
```rust
// Without default appearance
Input::new(&textarea)
Input::new(&state)
.appearance(false)
.h(px(200.))
@ -255,80 +234,25 @@ div()
.rounded_lg()
.p_4()
.child(
Input::new(&textarea)
Input::new(&state)
.appearance(false)
.h(px(150.))
)
```
## API Reference
### InputState (Multi-line Methods)
| Method | Description |
| -------------------------------------- | ------------------------------------------------ |
| `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 |
### Input (Multi-line Methods)
| Method | Description |
| ----------- | -------------------------- |
| `h(height)` | Set explicit height |
| `h_full()` | Take full available height |
### Position
| Field | Description |
| ----------- | ---------------------------------- |
| `line` | 0-based line number |
| `character` | 0-based character position in line |
### TabSize
| Field | Description |
| ----------- | ------------------------------------------ |
| `tab_size` | Number of spaces per tab (default: 2) |
| `hard_tabs` | Use actual tab characters (default: false) |
### Keyboard Shortcuts
| Shortcut | Action |
| ------------- | --------------------------- |
| `Enter` | Insert new line |
| `Shift+Enter` | Insert new line (secondary) |
| `Tab` | Indent line/selection |
| `Shift+Tab` | Outdent line/selection |
| `Ctrl/Cmd+A` | Select all |
| `Ctrl/Cmd+Z` | Undo |
| `Ctrl/Cmd+Y` | Redo |
| `Ctrl/Cmd+F` | Open search (if enabled) |
| `Ctrl/Cmd+[` | Outdent block |
| `Ctrl/Cmd+]` | Indent block |
## Examples
### Comment Box
```rust
struct CommentBox {
textarea: Entity<InputState>,
state: Entity<InputState>,
char_limit: usize,
}
impl CommentBox {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let textarea = cx.new(|cx|
let state = cx.new(|cx|
InputState::new(window, cx)
.auto_grow(3, 8)
.placeholder("Write your comment...")
@ -336,7 +260,7 @@ impl CommentBox {
);
Self {
textarea,
state,
char_limit: 500,
}
}
@ -344,13 +268,13 @@ impl CommentBox {
impl Render for CommentBox {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let content = self.textarea.read(cx).value();
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.textarea))
.child(Input::new(&self.state))
.child(
h_flex()
.justify_between()
@ -457,22 +381,3 @@ impl Render for TextEditor {
}
}
```
## Performance Notes
- Optimized for large text content (up to 200K 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