1use component::{example_group, single_example};
2
3use gpui::{App, FocusHandle, Focusable, Hsla, Length};
4use std::sync::Arc;
5
6use ui::Tooltip;
7use ui::prelude::*;
8
9use crate::ErasedEditor;
10
11pub struct InputFieldStyle {
12 text_color: Hsla,
13 background_color: Hsla,
14 border_color: Hsla,
15}
16
17/// An Input Field component that can be used to create text fields like search inputs, form fields, etc.
18///
19/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc.
20#[derive(RegisterComponent)]
21pub struct InputField {
22 /// An optional label for the text field.
23 ///
24 /// Its position is determined by the [`FieldLabelLayout`].
25 label: Option<SharedString>,
26 /// The size of the label text.
27 label_size: LabelSize,
28 /// The placeholder text for the text field.
29 placeholder: SharedString,
30
31 editor: Arc<dyn ErasedEditor>,
32 /// An optional icon that is displayed at the start of the text field.
33 ///
34 /// For example, a magnifying glass icon in a search field.
35 start_icon: Option<IconName>,
36 /// The minimum width of for the input
37 min_width: Length,
38 /// The tab index for keyboard navigation order.
39 tab_index: Option<isize>,
40 /// Whether this field is a tab stop (can be focused via Tab key).
41 tab_stop: bool,
42 /// Whether the field content is masked (for sensitive fields like passwords or API keys).
43 masked: Option<bool>,
44}
45
46impl Focusable for InputField {
47 fn focus_handle(&self, cx: &App) -> FocusHandle {
48 self.editor.focus_handle(cx)
49 }
50}
51
52impl InputField {
53 pub fn new(window: &mut Window, cx: &mut App, placeholder_text: &str) -> Self {
54 let editor_factory = crate::ERASED_EDITOR_FACTORY
55 .get()
56 .expect("ErasedEditorFactory to be initialized");
57 let editor = (editor_factory)(window, cx);
58 editor.set_placeholder_text(placeholder_text, window, cx);
59
60 Self {
61 label: None,
62 label_size: LabelSize::Small,
63 placeholder: SharedString::new(placeholder_text),
64 editor,
65 start_icon: None,
66 min_width: px(192.).into(),
67 tab_index: None,
68 tab_stop: true,
69 masked: None,
70 }
71 }
72
73 pub fn start_icon(mut self, icon: IconName) -> Self {
74 self.start_icon = Some(icon);
75 self
76 }
77
78 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
79 self.label = Some(label.into());
80 self
81 }
82
83 pub fn label_size(mut self, size: LabelSize) -> Self {
84 self.label_size = size;
85 self
86 }
87
88 pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
89 self.min_width = width.into();
90 self
91 }
92
93 pub fn tab_index(mut self, index: isize) -> Self {
94 self.tab_index = Some(index);
95 self
96 }
97
98 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
99 self.tab_stop = tab_stop;
100 self
101 }
102
103 /// Sets this field as a masked/sensitive input (e.g., for passwords or API keys).
104 pub fn masked(mut self, masked: bool) -> Self {
105 self.masked = Some(masked);
106 self
107 }
108
109 pub fn is_empty(&self, cx: &App) -> bool {
110 self.editor().text(cx).trim().is_empty()
111 }
112
113 pub fn editor(&self) -> &Arc<dyn ErasedEditor> {
114 &self.editor
115 }
116
117 pub fn text(&self, cx: &App) -> String {
118 self.editor().text(cx)
119 }
120
121 pub fn clear(&self, window: &mut Window, cx: &mut App) {
122 self.editor().clear(window, cx)
123 }
124
125 pub fn set_text(&self, text: &str, window: &mut Window, cx: &mut App) {
126 self.editor().set_text(text, window, cx)
127 }
128
129 pub fn set_masked(&self, masked: bool, window: &mut Window, cx: &mut App) {
130 self.editor().set_masked(masked, window, cx)
131 }
132}
133
134impl Render for InputField {
135 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
136 let editor = self.editor.clone();
137
138 if let Some(masked) = self.masked {
139 self.editor.set_masked(masked, window, cx);
140 }
141
142 let theme_color = cx.theme().colors();
143
144 let style = InputFieldStyle {
145 text_color: theme_color.text,
146 background_color: theme_color.editor_background,
147 border_color: theme_color.border_variant,
148 };
149
150 let focus_handle = self.editor.focus_handle(cx);
151
152 let configured_handle = if let Some(tab_index) = self.tab_index {
153 focus_handle.tab_index(tab_index).tab_stop(self.tab_stop)
154 } else if !self.tab_stop {
155 focus_handle.tab_stop(false)
156 } else {
157 focus_handle
158 };
159
160 v_flex()
161 .id(self.placeholder.clone())
162 .w_full()
163 .gap_1()
164 .when_some(self.label.clone(), |this, label| {
165 this.child(
166 Label::new(label)
167 .size(self.label_size)
168 .color(Color::Default),
169 )
170 })
171 .child(
172 h_flex()
173 .track_focus(&configured_handle)
174 .min_w(self.min_width)
175 .min_h_8()
176 .w_full()
177 .px_2()
178 .py_1p5()
179 .flex_grow()
180 .text_color(style.text_color)
181 .rounded_md()
182 .bg(style.background_color)
183 .border_1()
184 .border_color(style.border_color)
185 .when(
186 editor.focus_handle(cx).contains_focused(window, cx),
187 |this| this.border_color(theme_color.border_focused),
188 )
189 .when_some(self.start_icon, |this, icon| {
190 this.gap_1()
191 .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
192 })
193 .child(self.editor.render(window, cx))
194 .when_some(self.masked, |this, is_masked| {
195 this.child(
196 IconButton::new(
197 "toggle-masked",
198 if is_masked {
199 IconName::Eye
200 } else {
201 IconName::EyeOff
202 },
203 )
204 .icon_size(IconSize::Small)
205 .icon_color(Color::Muted)
206 .tooltip(Tooltip::text(if is_masked { "Show" } else { "Hide" }))
207 .on_click(cx.listener(
208 |this, _, window, cx| {
209 if let Some(ref mut masked) = this.masked {
210 *masked = !*masked;
211 this.editor.set_masked(*masked, window, cx);
212 cx.notify();
213 }
214 },
215 )),
216 )
217 }),
218 )
219 }
220}
221
222impl Component for InputField {
223 fn scope() -> ComponentScope {
224 ComponentScope::Input
225 }
226
227 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
228 let input_small =
229 cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label"));
230
231 let input_regular = cx.new(|cx| {
232 InputField::new(window, cx, "placeholder")
233 .label("Regular Label")
234 .label_size(LabelSize::Default)
235 });
236
237 Some(
238 v_flex()
239 .gap_6()
240 .children(vec![example_group(vec![
241 single_example(
242 "Small Label (Default)",
243 div().child(input_small).into_any_element(),
244 ),
245 single_example(
246 "Regular Label",
247 div().child(input_regular).into_any_element(),
248 ),
249 ])])
250 .into_any_element(),
251 )
252 }
253}