1use gpui::prelude::*;
7use gpui::{
8 div, px, Context, EventEmitter, FocusHandle, IntoElement, KeyDownEvent, MouseButton,
9 SharedString, Window,
10};
11
12use super::{apply_key, control_metrics, edit::TextEdit, KeyOutcome};
13use crate::theme::{theme, ColorName, Size};
14
15#[derive(Debug, Clone)]
17pub enum TextInputEvent {
18 Change(String),
20 Submit(String),
22}
23
24pub struct TextInput {
26 edit: TextEdit,
27 focus: FocusHandle,
28 placeholder: SharedString,
29 label: Option<SharedString>,
30 description: Option<SharedString>,
31 error: Option<SharedString>,
32 size: Size,
33 radius: Option<Size>,
34 disabled: bool,
35 password: bool,
36}
37
38impl EventEmitter<TextInputEvent> for TextInput {}
39
40impl TextInput {
41 pub fn new(cx: &mut Context<Self>) -> Self {
42 TextInput {
43 edit: TextEdit::new(""),
44 focus: cx.focus_handle(),
45 placeholder: SharedString::default(),
46 label: None,
47 description: None,
48 error: None,
49 size: Size::Sm,
50 radius: None,
51 disabled: false,
52 password: false,
53 }
54 }
55
56 pub fn value(mut self, value: &str) -> Self {
57 self.edit = TextEdit::new(value);
58 self
59 }
60
61 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
62 self.placeholder = placeholder.into();
63 self
64 }
65
66 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
67 self.label = Some(label.into());
68 self
69 }
70
71 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
72 self.description = Some(description.into());
73 self
74 }
75
76 pub fn error(mut self, error: impl Into<SharedString>) -> Self {
77 self.error = Some(error.into());
78 self
79 }
80
81 pub fn size(mut self, size: Size) -> Self {
82 self.size = size;
83 self
84 }
85
86 pub fn radius(mut self, radius: Size) -> Self {
87 self.radius = Some(radius);
88 self
89 }
90
91 pub fn disabled(mut self, disabled: bool) -> Self {
92 self.disabled = disabled;
93 self
94 }
95
96 pub fn password(mut self, password: bool) -> Self {
97 self.password = password;
98 self
99 }
100
101 pub fn focus_handle(&self) -> FocusHandle {
103 self.focus.clone()
104 }
105
106 pub fn text(&self) -> String {
108 self.edit.text()
109 }
110
111 pub fn set_text(&mut self, value: &str, cx: &mut Context<Self>) {
113 self.edit = TextEdit::new(value);
114 cx.notify();
115 }
116
117 fn on_key(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
118 if self.disabled {
119 return;
120 }
121 match apply_key(&mut self.edit, &event.keystroke) {
122 KeyOutcome::Submit => {
123 cx.emit(TextInputEvent::Submit(self.edit.text()));
124 cx.notify();
125 cx.stop_propagation();
126 }
127 KeyOutcome::Edited => {
128 cx.emit(TextInputEvent::Change(self.edit.text()));
129 cx.notify();
130 cx.stop_propagation();
131 }
132 KeyOutcome::Cancel | KeyOutcome::Pass => {}
135 }
136 }
137}
138
139impl Render for TextInput {
140 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
141 let t = theme(cx);
142 let (height, pad_x, font) = control_metrics(self.size);
143 let radius = t.radius(self.radius.unwrap_or(t.default_radius));
144 let focused = self.focus.is_focused(window) && !self.disabled;
145 let has_error = self.error.is_some();
146
147 let border = if has_error {
148 t.color(ColorName::Red, 6)
149 } else if focused {
150 t.primary()
151 } else {
152 t.border()
153 };
154 let text_color = t.text().hsla();
155 let dimmed = t.dimmed().hsla();
156 let surface = t.surface().hsla();
157 let caret_color = t.primary().hsla();
158 let error_color = t.color(ColorName::Red, if t.scheme.is_dark() { 5 } else { 7 }).hsla();
159 let border = border.hsla();
160 let font_sm = t.font_size(Size::Sm);
161 let font_xs = t.font_size(Size::Xs);
162
163 let mask = |s: String| {
164 if self.password {
165 "\u{2022}".repeat(s.chars().count())
166 } else {
167 s
168 }
169 };
170
171 let interior = if focused {
173 let (before, after) = self.edit.split();
174 div()
175 .flex()
176 .items_center()
177 .text_color(text_color)
178 .child(SharedString::from(mask(before)))
179 .child(
180 div()
181 .w(px(1.0))
182 .h(px(font * 1.15))
183 .bg(caret_color),
184 )
185 .child(SharedString::from(mask(after)))
186 } else if self.edit.is_empty() {
187 div()
188 .text_color(dimmed)
189 .child(self.placeholder.clone())
190 } else {
191 div()
192 .text_color(text_color)
193 .child(SharedString::from(mask(self.edit.text())))
194 };
195
196 let field = div()
197 .id("guise-textinput")
198 .track_focus(&self.focus)
199 .on_key_down(cx.listener(Self::on_key))
200 .on_mouse_down(
201 MouseButton::Left,
202 cx.listener(|this, _ev, window, cx| {
203 window.focus(&this.focus);
204 cx.notify();
205 }),
206 )
207 .flex()
208 .items_center()
209 .h(px(height))
210 .px(px(pad_x))
211 .rounded(px(radius))
212 .border_1()
213 .border_color(border)
214 .bg(surface)
215 .text_size(px(font))
216 .child(interior);
217
218 let mut column = div().flex().flex_col().gap(px(4.0));
219 if let Some(label) = self.label.clone() {
220 column = column.child(
221 div()
222 .text_size(px(font_sm))
223 .text_color(text_color)
224 .child(label),
225 );
226 }
227 column = column.child(field);
228 if let Some(error) = self.error.clone() {
229 column = column.child(
230 div()
231 .text_size(px(font_xs))
232 .text_color(error_color)
233 .child(error),
234 );
235 } else if let Some(description) = self.description.clone() {
236 column = column.child(
237 div()
238 .text_size(px(font_xs))
239 .text_color(dimmed)
240 .child(description),
241 );
242 }
243
244 if self.disabled {
245 column.opacity(0.6)
246 } else {
247 column
248 }
249 }
250}