ui/components/
text_input.rs1use gpui::{Context, FocusHandle, Focusable, KeyDownEvent, MouseButton, Render};
2
3use crate::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum InputValidationState {
9 #[default]
10 Neutral,
11 Error,
12 Success,
13 Warning,
14}
15
16pub 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 pub fn read_only(mut self, read_only: bool) -> Self {
56 self.read_only = read_only;
57 self
58 }
59
60 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 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 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 pub fn text(&self) -> &str {
95 &self.content
96 }
97
98 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 pub fn clear(&mut self, cx: &mut Context<Self>) {
108 self.content.clear();
109 cx.notify();
110 }
111
112 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 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
210pub type Textarea = TextInput;