Skip to main content

ui/components/
text_input.rs

1use gpui::{Context, FocusHandle, Focusable, KeyDownEvent, MouseButton, Render};
2
3use crate::prelude::*;
4
5/// Visual validation state of a [`TextInput`], reflected in its border/focus
6/// ring color.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum InputValidationState {
9    #[default]
10    Neutral,
11    Error,
12    Success,
13    Warning,
14}
15
16/// A focusable single-line (or multi-line) text field backed by a real
17/// `String` buffer. Keyboard input is handled via key events (`key_char` +
18/// editing keys), so typed characters genuinely appear and backspace deletes.
19///
20/// This is a stateful view: create with `cx.new(|cx| TextInput::new(cx))` and
21/// store the resulting `Entity<TextInput>`.
22pub struct TextInput {
23    content: String,
24    placeholder: SharedString,
25    focus_handle: FocusHandle,
26    multiline: bool,
27    validation: InputValidationState,
28    read_only: bool,
29}
30
31impl TextInput {
32    pub fn new(cx: &mut App) -> Self {
33        Self {
34            content: String::new(),
35            placeholder: SharedString::default(),
36            focus_handle: cx.focus_handle(),
37            multiline: false,
38            validation: InputValidationState::Neutral,
39            read_only: false,
40        }
41    }
42
43    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
44        self.placeholder = placeholder.into();
45        self
46    }
47
48    pub fn multiline(mut self, multiline: bool) -> Self {
49        self.multiline = multiline;
50        self
51    }
52
53    /// When `true`, the input no longer accepts keyboard edits (used for
54    /// read-only code previews). Focus/selection styling is unaffected.
55    pub fn read_only(mut self, read_only: bool) -> Self {
56        self.read_only = read_only;
57        self
58    }
59
60    /// Sets the error validation state (red border/ring) when `invalid` is
61    /// true, otherwise clears back to [`InputValidationState::Neutral`].
62    pub fn invalid(mut self, invalid: bool) -> Self {
63        self.validation = if invalid {
64            InputValidationState::Error
65        } else {
66            InputValidationState::Neutral
67        };
68        self
69    }
70
71    /// Sets the success validation state (green border/ring) when `success`
72    /// is true, otherwise clears back to [`InputValidationState::Neutral`].
73    pub fn success(mut self, success: bool) -> Self {
74        self.validation = if success {
75            InputValidationState::Success
76        } else {
77            InputValidationState::Neutral
78        };
79        self
80    }
81
82    /// Sets the warning validation state (amber border/ring) when `warning`
83    /// is true, otherwise clears back to [`InputValidationState::Neutral`].
84    pub fn warning(mut self, warning: bool) -> Self {
85        self.validation = if warning {
86            InputValidationState::Warning
87        } else {
88            InputValidationState::Neutral
89        };
90        self
91    }
92
93    /// The current text content.
94    pub fn text(&self) -> &str {
95        &self.content
96    }
97
98    /// Programmatically sets the text content (e.g. `SearchInput`/`Combobox`
99    /// setting the display text after a selection). Notifies for re-render.
100    pub fn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
101        self.content = text.into();
102        cx.notify();
103    }
104
105    /// Clears the text content (e.g. `SearchInput`'s clear button). Notifies
106    /// for re-render.
107    pub fn clear(&mut self, cx: &mut Context<Self>) {
108        self.content.clear();
109        cx.notify();
110    }
111
112    /// Dynamically toggles read-only mode after construction (e.g.
113    /// `CodeEditor` switching an already-created input between editable and
114    /// preview modes). Notifies for re-render.
115    pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
116        self.read_only = read_only;
117        cx.notify();
118    }
119
120    fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
121        if self.read_only {
122            return;
123        }
124        let keystroke = &event.keystroke;
125        // Ignore keyboard shortcuts (cmd/ctrl chords) — only capture text input.
126        if keystroke.modifiers.control || keystroke.modifiers.platform {
127            return;
128        }
129        match keystroke.key.as_str() {
130            "backspace" => {
131                self.content.pop();
132            }
133            "enter" if self.multiline => self.content.push('\n'),
134            "space" => self.content.push(' '),
135            _ => {
136                if let Some(text) = &keystroke.key_char {
137                    self.content.push_str(text);
138                }
139            }
140        }
141        cx.notify();
142    }
143}
144
145impl Render for TextInput {
146    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
147        let focused = self.focus_handle.is_focused(window);
148        let is_empty = self.content.is_empty();
149        let display: SharedString = if is_empty {
150            self.placeholder.clone()
151        } else {
152            self.content.clone().into()
153        };
154        let text_color = if is_empty {
155            semantic::text_placeholder(cx)
156        } else {
157            semantic::text(cx)
158        };
159        let border_color = match self.validation {
160            InputValidationState::Error => palette::danger(500),
161            InputValidationState::Success => palette::success(500),
162            InputValidationState::Warning => palette::warning(500),
163            InputValidationState::Neutral => semantic::border(cx),
164        };
165        let ring_color = match self.validation {
166            InputValidationState::Error => palette::danger(500),
167            InputValidationState::Success => palette::success(500),
168            InputValidationState::Warning => palette::warning(500),
169            InputValidationState::Neutral => palette::primary(500),
170        };
171
172        let field = div()
173            .track_focus(&self.focus_handle)
174            .on_key_down(cx.listener(Self::on_key_down))
175            .on_mouse_down(
176                MouseButton::Left,
177                cx.listener(|this, _event, window, cx| {
178                    window.focus(&this.focus_handle, cx);
179                    cx.notify();
180                }),
181            )
182            .w_full()
183            .when(self.multiline, |this| this.min_h(px(96.)))
184            .flex()
185            .flex_wrap()
186            .items_center()
187            .gap_0p5()
188            .px_3()
189            .py_2()
190            .rounded_md()
191            .bg(semantic::surface(cx))
192            .border_1()
193            .border_color(border_color)
194            .text_color(text_color)
195            .child(display)
196            .when(focused && !is_empty && !self.read_only, |this| {
197                this.child(div().w(px(1.)).h(px(16.)).bg(palette::primary(500)))
198            });
199
200        focus_ring(field, focused, ring_color)
201    }
202}
203
204impl Focusable for TextInput {
205    fn focus_handle(&self, _cx: &App) -> FocusHandle {
206        self.focus_handle.clone()
207    }
208}
209
210/// A multi-line text field. Construct with
211/// `cx.new(|cx| Textarea::new(cx).multiline(true))`.
212pub type Textarea = TextInput;