Skip to main content

ui/components/
text_input.rs

1use std::rc::Rc;
2
3use gpui::{AnyElement, Context, FocusHandle, Focusable, KeyDownEvent, MouseButton, Render};
4
5use crate::prelude::*;
6
7/// Visual validation state of a [`TextInput`], reflected in its border/focus
8/// ring color.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum InputValidationState {
11    #[default]
12    Neutral,
13    Error,
14    Success,
15    Warning,
16}
17
18/// A focusable single-line (or multi-line) text field backed by a real
19/// `String` buffer. Keyboard input is handled via key events (`key_char` +
20/// editing keys), so typed characters genuinely appear and backspace deletes.
21///
22/// This is a stateful view: create with `cx.new(|cx| TextInput::new(cx))` and
23/// store the resulting `Entity<TextInput>`.
24pub struct TextInput {
25    content: String,
26    placeholder: SharedString,
27    focus_handle: FocusHandle,
28    multiline: bool,
29    submit_on_enter: bool,
30    validation: InputValidationState,
31    read_only: bool,
32    on_submit: Option<Rc<dyn Fn(&mut Window, &mut Context<Self>) + 'static>>,
33}
34
35impl TextInput {
36    pub fn new(cx: &mut App) -> Self {
37        Self {
38            content: String::new(),
39            placeholder: SharedString::default(),
40            focus_handle: cx.focus_handle(),
41            multiline: false,
42            submit_on_enter: false,
43            validation: InputValidationState::Neutral,
44            read_only: false,
45            on_submit: None,
46        }
47    }
48
49    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
50        self.placeholder = placeholder.into();
51        self
52    }
53
54    pub fn multiline(mut self, multiline: bool) -> Self {
55        self.multiline = multiline;
56        self
57    }
58
59    /// When `true`, plain Enter fires [`Self::on_submit`] instead of
60    /// inserting a newline; Shift/Ctrl/Cmd+Enter still inserts a newline (in
61    /// `multiline` mode). Defaults to `false`, which preserves the previous
62    /// behavior of always inserting a newline on Enter in `multiline` mode
63    /// (e.g. `CodeEditor`'s free-form multiline input).
64    pub fn submit_on_enter(mut self, submit_on_enter: bool) -> Self {
65        self.submit_on_enter = submit_on_enter;
66        self
67    }
68
69    /// Registers a callback fired when the user presses plain Enter while
70    /// [`Self::submit_on_enter`] is `true`. Has no effect otherwise.
71    pub fn on_submit(
72        mut self,
73        handler: impl Fn(&mut Window, &mut Context<Self>) + 'static,
74    ) -> Self {
75        self.on_submit = Some(Rc::new(handler));
76        self
77    }
78
79    /// When `true`, the input no longer accepts keyboard edits (used for
80    /// read-only code previews). Focus/selection styling is unaffected.
81    pub fn read_only(mut self, read_only: bool) -> Self {
82        self.read_only = read_only;
83        self
84    }
85
86    /// Sets the error validation state (red border/ring) when `invalid` is
87    /// true, otherwise clears back to [`InputValidationState::Neutral`].
88    pub fn invalid(mut self, invalid: bool) -> Self {
89        self.validation = if invalid {
90            InputValidationState::Error
91        } else {
92            InputValidationState::Neutral
93        };
94        self
95    }
96
97    /// Sets the success validation state (green border/ring) when `success`
98    /// is true, otherwise clears back to [`InputValidationState::Neutral`].
99    pub fn success(mut self, success: bool) -> Self {
100        self.validation = if success {
101            InputValidationState::Success
102        } else {
103            InputValidationState::Neutral
104        };
105        self
106    }
107
108    /// Sets the warning validation state (amber border/ring) when `warning`
109    /// is true, otherwise clears back to [`InputValidationState::Neutral`].
110    pub fn warning(mut self, warning: bool) -> Self {
111        self.validation = if warning {
112            InputValidationState::Warning
113        } else {
114            InputValidationState::Neutral
115        };
116        self
117    }
118
119    /// The current text content.
120    pub fn text(&self) -> &str {
121        &self.content
122    }
123
124    /// Programmatically sets the text content (e.g. `SearchInput`/`Combobox`
125    /// setting the display text after a selection). Notifies for re-render.
126    pub fn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
127        self.content = text.into();
128        cx.notify();
129    }
130
131    /// Clears the text content (e.g. `SearchInput`'s clear button). Notifies
132    /// for re-render.
133    pub fn clear(&mut self, cx: &mut Context<Self>) {
134        self.content.clear();
135        cx.notify();
136    }
137
138    /// Dynamically toggles read-only mode after construction (e.g.
139    /// `CodeEditor` switching an already-created input between editable and
140    /// preview modes). Notifies for re-render.
141    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
142        self.read_only = read_only;
143        cx.notify();
144    }
145
146    fn on_key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
147        if self.read_only {
148            return;
149        }
150        let keystroke = &event.keystroke;
151
152        if self.submit_on_enter && keystroke.key == "enter" {
153            let wants_newline = self.multiline
154                && (keystroke.modifiers.shift
155                    || keystroke.modifiers.control
156                    || keystroke.modifiers.platform);
157            if wants_newline {
158                self.content.push('\n');
159            } else if let Some(on_submit) = self.on_submit.clone() {
160                on_submit(window, cx);
161            }
162            cx.notify();
163            return;
164        }
165
166        // Ignore keyboard shortcuts (cmd/ctrl chords) — only capture text input.
167        if keystroke.modifiers.control || keystroke.modifiers.platform {
168            return;
169        }
170        match keystroke.key.as_str() {
171            "backspace" => {
172                self.content.pop();
173            }
174            "enter" if self.multiline => self.content.push('\n'),
175            "space" => self.content.push(' '),
176            _ => {
177                if let Some(text) = &keystroke.key_char {
178                    self.content.push_str(text);
179                }
180            }
181        }
182        cx.notify();
183    }
184}
185
186impl Render for TextInput {
187    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
188        let focused = self.focus_handle.is_focused(window);
189        let is_empty = self.content.is_empty();
190        let text_color = if is_empty {
191            semantic::text_placeholder(cx)
192        } else {
193            semantic::text(cx)
194        };
195        let border_color = match self.validation {
196            InputValidationState::Error => palette::danger(500),
197            InputValidationState::Success => palette::success(500),
198            InputValidationState::Warning => palette::warning(500),
199            InputValidationState::Neutral => semantic::border(cx),
200        };
201        let ring_color = match self.validation {
202            InputValidationState::Error => palette::danger(500),
203            InputValidationState::Success => palette::success(500),
204            InputValidationState::Warning => palette::warning(500),
205            InputValidationState::Neutral => palette::primary(500),
206        };
207        let show_cursor = focused && !is_empty && !self.read_only;
208        let cursor = || div().w(px(1.)).h(px(16.)).bg(palette::primary(500));
209
210        // Multiline content is split and rendered one row per line (rather
211        // than a single text child carrying embedded `\n`s) so each typed
212        // newline reliably produces a new visual row, with the blinking
213        // caret appended to the last row.
214        let content: AnyElement = if self.multiline {
215            let text: SharedString = if is_empty {
216                self.placeholder.clone()
217            } else {
218                self.content.clone().into()
219            };
220            let lines: Vec<String> = text.split('\n').map(str::to_string).collect();
221            let last_ix = lines.len().saturating_sub(1);
222            v_flex()
223                .w_full()
224                .children(lines.into_iter().enumerate().map(|(ix, line)| {
225                    h_flex()
226                        .min_h(px(20.))
227                        .items_center()
228                        .gap_0p5()
229                        .child(SharedString::from(line))
230                        .when(ix == last_ix && show_cursor, |this| this.child(cursor()))
231                }))
232                .into_any_element()
233        } else {
234            let display: SharedString = if is_empty {
235                self.placeholder.clone()
236            } else {
237                self.content.clone().into()
238            };
239            h_flex()
240                .flex_wrap()
241                .items_center()
242                .gap_0p5()
243                .child(display)
244                .when(show_cursor, |this| this.child(cursor()))
245                .into_any_element()
246        };
247
248        let field = div()
249            .track_focus(&self.focus_handle)
250            .on_key_down(cx.listener(Self::on_key_down))
251            .on_mouse_down(
252                MouseButton::Left,
253                cx.listener(|this, _event, window, cx| {
254                    window.focus(&this.focus_handle, cx);
255                    cx.notify();
256                }),
257            )
258            .w_full()
259            .when(self.multiline, |this| this.min_h(px(96.)))
260            .px_3()
261            .py_2()
262            .rounded_md()
263            .bg(semantic::surface(cx))
264            .border_1()
265            .border_color(border_color)
266            .text_color(text_color)
267            .child(content);
268
269        focus_ring(field, focused, ring_color)
270    }
271}
272
273impl Focusable for TextInput {
274    fn focus_handle(&self, _cx: &App) -> FocusHandle {
275        self.focus_handle.clone()
276    }
277}
278
279/// A multi-line text field. Construct with
280/// `cx.new(|cx| Textarea::new(cx).multiline(true))`.
281pub type Textarea = TextInput;