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}
29
30impl TextInput {
31    pub fn new(cx: &mut App) -> Self {
32        Self {
33            content: String::new(),
34            placeholder: SharedString::default(),
35            focus_handle: cx.focus_handle(),
36            multiline: false,
37            validation: InputValidationState::Neutral,
38        }
39    }
40
41    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
42        self.placeholder = placeholder.into();
43        self
44    }
45
46    pub fn multiline(mut self, multiline: bool) -> Self {
47        self.multiline = multiline;
48        self
49    }
50
51    /// Sets the error validation state (red border/ring) when `invalid` is
52    /// true, otherwise clears back to [`InputValidationState::Neutral`].
53    pub fn invalid(mut self, invalid: bool) -> Self {
54        self.validation = if invalid {
55            InputValidationState::Error
56        } else {
57            InputValidationState::Neutral
58        };
59        self
60    }
61
62    /// Sets the success validation state (green border/ring) when `success`
63    /// is true, otherwise clears back to [`InputValidationState::Neutral`].
64    pub fn success(mut self, success: bool) -> Self {
65        self.validation = if success {
66            InputValidationState::Success
67        } else {
68            InputValidationState::Neutral
69        };
70        self
71    }
72
73    /// Sets the warning validation state (amber border/ring) when `warning`
74    /// is true, otherwise clears back to [`InputValidationState::Neutral`].
75    pub fn warning(mut self, warning: bool) -> Self {
76        self.validation = if warning {
77            InputValidationState::Warning
78        } else {
79            InputValidationState::Neutral
80        };
81        self
82    }
83
84    /// The current text content.
85    pub fn text(&self) -> &str {
86        &self.content
87    }
88
89    /// Programmatically sets the text content (e.g. `SearchInput`/`Combobox`
90    /// setting the display text after a selection). Notifies for re-render.
91    pub fn set_text(&mut self, text: impl Into<String>, cx: &mut Context<Self>) {
92        self.content = text.into();
93        cx.notify();
94    }
95
96    /// Clears the text content (e.g. `SearchInput`'s clear button). Notifies
97    /// for re-render.
98    pub fn clear(&mut self, cx: &mut Context<Self>) {
99        self.content.clear();
100        cx.notify();
101    }
102
103    fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
104        let keystroke = &event.keystroke;
105        // Ignore keyboard shortcuts (cmd/ctrl chords) — only capture text input.
106        if keystroke.modifiers.control || keystroke.modifiers.platform {
107            return;
108        }
109        match keystroke.key.as_str() {
110            "backspace" => {
111                self.content.pop();
112            }
113            "enter" if self.multiline => self.content.push('\n'),
114            "space" => self.content.push(' '),
115            _ => {
116                if let Some(text) = &keystroke.key_char {
117                    self.content.push_str(text);
118                }
119            }
120        }
121        cx.notify();
122    }
123}
124
125impl Render for TextInput {
126    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
127        let focused = self.focus_handle.is_focused(window);
128        let is_empty = self.content.is_empty();
129        let display: SharedString = if is_empty {
130            self.placeholder.clone()
131        } else {
132            self.content.clone().into()
133        };
134        let text_color = if is_empty {
135            semantic::text_placeholder(cx)
136        } else {
137            semantic::text(cx)
138        };
139        let border_color = match self.validation {
140            InputValidationState::Error => palette::danger(500),
141            InputValidationState::Success => palette::success(500),
142            InputValidationState::Warning => palette::warning(500),
143            InputValidationState::Neutral => semantic::border(cx),
144        };
145        let ring_color = match self.validation {
146            InputValidationState::Error => palette::danger(500),
147            InputValidationState::Success => palette::success(500),
148            InputValidationState::Warning => palette::warning(500),
149            InputValidationState::Neutral => palette::primary(500),
150        };
151
152        let field = div()
153            .track_focus(&self.focus_handle)
154            .on_key_down(cx.listener(Self::on_key_down))
155            .on_mouse_down(
156                MouseButton::Left,
157                cx.listener(|this, _event, window, cx| {
158                    window.focus(&this.focus_handle, cx);
159                    cx.notify();
160                }),
161            )
162            .w_full()
163            .when(self.multiline, |this| this.min_h(px(96.)))
164            .flex()
165            .flex_wrap()
166            .items_center()
167            .gap_0p5()
168            .px_3()
169            .py_2()
170            .rounded_md()
171            .bg(semantic::surface(cx))
172            .border_1()
173            .border_color(border_color)
174            .text_color(text_color)
175            .child(display)
176            .when(focused && !is_empty, |this| {
177                this.child(div().w(px(1.)).h(px(16.)).bg(palette::primary(500)))
178            });
179
180        focus_ring(field, focused, ring_color)
181    }
182}
183
184impl Focusable for TextInput {
185    fn focus_handle(&self, _cx: &App) -> FocusHandle {
186        self.focus_handle.clone()
187    }
188}
189
190/// A multi-line text field. Construct with
191/// `cx.new(|cx| Textarea::new(cx).multiline(true))`.
192pub type Textarea = TextInput;