ui/components/
text_input.rs1use std::rc::Rc;
2
3use gpui::{AnyElement, Context, FocusHandle, Focusable, KeyDownEvent, MouseButton, Render};
4
5use crate::prelude::*;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum InputValidationState {
11 #[default]
12 Neutral,
13 Error,
14 Success,
15 Warning,
16}
17
18pub 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 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 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 pub fn read_only(mut self, read_only: bool) -> Self {
82 self.read_only = read_only;
83 self
84 }
85
86 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 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 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 pub fn text(&self) -> &str {
121 &self.content
122 }
123
124 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 pub fn clear(&mut self, cx: &mut Context<Self>) {
134 self.content.clear();
135 cx.notify();
136 }
137
138 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 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 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
279pub type Textarea = TextInput;