From 7e479aa7f2ea61b4cb57c14d878d71d57606b87c Mon Sep 17 00:00:00 2001 From: Andreas Johansson Date: Fri, 28 Nov 2025 12:51:40 +0100 Subject: [PATCH] input: Add to support CodeEditor as single line mode. (#1696) ## Screenshot From `input_story`: Screenshot From 2025-11-28 09-14-03 ## 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, Image 2) In my SQL editor I want to be able to allow JSON syntax highlighting for inline editing of json columns, Image --------- Co-authored-by: Jason Lee --- crates/story/examples/large-text.rs | 2 +- crates/story/src/input_story.rs | 14 ++ crates/story/src/textarea_story.rs | 4 +- crates/ui/src/input/element.rs | 14 +- crates/ui/src/input/indent.rs | 36 ++-- crates/ui/src/input/mode.rs | 180 ++++++++++++++++++-- crates/ui/src/input/state.rs | 45 ++--- docs/docs/components/editor.md | 247 +++++++++------------------- 8 files changed, 302 insertions(+), 240 deletions(-) diff --git a/crates/story/examples/large-text.rs b/crates/story/examples/large-text.rs index 3a6d63ef..9cfb8714 100644 --- a/crates/story/examples/large-text.rs +++ b/crates/story/examples/large-text.rs @@ -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, diff --git a/crates/story/src/input_story.rs b/crates/story/src/input_story.rs index 3a1fd343..a645fba4 100644 --- a/crates/story/src/input_story.rs +++ b/crates/story/src/input_story.rs @@ -23,6 +23,7 @@ pub struct InputStory { mask_input2: Entity, currency_input: Entity, custom_input: Entity, + code_input: Entity, _subscriptions: Vec, } @@ -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)), + ) } } diff --git a/crates/story/src/textarea_story.rs b/crates/story/src/textarea_story.rs index 0dd22460..f2c63d44 100644 --- a/crates/story/src/textarea_story.rs +++ b/crates/story/src/textarea_story.rs @@ -47,7 +47,7 @@ impl TextareaStory { fn new(window: &mut Window, cx: &mut Context) -> 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") diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 002086bd..05c3b18f 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -567,12 +567,12 @@ impl TextElement { bg_segments: &[(Range, Hsla)], window: &mut Window, ) -> Vec { - 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, 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(); diff --git a/crates/ui/src/input/indent.rs b/crates/ui/src/input/indent.rs index cbfba7c4..0ddaf69c 100644 --- a/crates/ui/src/input/indent.rs +++ b/crates/ui/src/input/indent.rs @@ -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, _ => {} } diff --git a/crates/ui/src/input/mode.rs b/crates/ui/src/input/mode.rs index 4017281f..2ed2a5c8 100644 --- a/crates/ui/src/input/mode.rs +++ b/crates/ui/src/input/mode.rs @@ -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) -> 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); + } +} diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index e865f215..35c358dd 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -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) -> 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) { - 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 } diff --git a/docs/docs/components/editor.md b/docs/docs/components/editor.md index dbf18762..d26420fc 100644 --- a/docs/docs/components/editor.md +++ b/docs/docs/components/editor.md @@ -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, -} - -impl Render for TextareaView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> 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, + state: Entity, char_limit: usize, } impl CommentBox { fn new(window: &mut Window, cx: &mut Context) -> 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) -> 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