1use component::{example_group, single_example};
2use editor::{Editor, EditorElement, EditorStyle};
3use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle};
4use settings::Settings;
5use std::sync::Arc;
6use theme::ThemeSettings;
7use ui::prelude::*;
8
9pub struct InputFieldStyle {
10 text_color: Hsla,
11 background_color: Hsla,
12 border_color: Hsla,
13}
14
15/// An Input Field component that can be used to create text fields like search inputs, form fields, etc.
16///
17/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc.
18#[derive(RegisterComponent)]
19pub struct InputField {
20 /// An optional label for the text field.
21 ///
22 /// Its position is determined by the [`FieldLabelLayout`].
23 label: Option<SharedString>,
24 /// The size of the label text.
25 label_size: LabelSize,
26 /// The placeholder text for the text field.
27 placeholder: SharedString,
28 /// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API.
29 ///
30 /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases.
31 pub editor: Entity<Editor>,
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 /// Whether the text field is disabled.
37 disabled: bool,
38 /// The minimum width of for the input
39 min_width: Length,
40 /// The tab index for keyboard navigation order.
41 tab_index: Option<isize>,
42 /// Whether this field is a tab stop (can be focused via Tab key).
43 tab_stop: 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: impl Into<SharedString>) -> Self {
54 let placeholder_text = placeholder.into();
55
56 let editor = cx.new(|cx| {
57 let mut input = Editor::single_line(window, cx);
58 input.set_placeholder_text(&placeholder_text, window, cx);
59 input
60 });
61
62 Self {
63 label: None,
64 label_size: LabelSize::Small,
65 placeholder: placeholder_text,
66 editor,
67 start_icon: None,
68 disabled: false,
69 min_width: px(192.).into(),
70 tab_index: None,
71 tab_stop: true,
72 }
73 }
74
75 pub fn start_icon(mut self, icon: IconName) -> Self {
76 self.start_icon = Some(icon);
77 self
78 }
79
80 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
81 self.label = Some(label.into());
82 self
83 }
84
85 pub fn label_size(mut self, size: LabelSize) -> Self {
86 self.label_size = size;
87 self
88 }
89
90 pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
91 self.min_width = width.into();
92 self
93 }
94
95 pub fn tab_index(mut self, index: isize) -> Self {
96 self.tab_index = Some(index);
97 self
98 }
99
100 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
101 self.tab_stop = tab_stop;
102 self
103 }
104
105 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
106 self.disabled = disabled;
107 self.editor
108 .update(cx, |editor, _| editor.set_read_only(disabled))
109 }
110
111 pub fn is_empty(&self, cx: &App) -> bool {
112 self.editor().read(cx).text(cx).trim().is_empty()
113 }
114
115 pub fn editor(&self) -> &Entity<Editor> {
116 &self.editor
117 }
118
119 pub fn text(&self, cx: &App) -> String {
120 self.editor().read(cx).text(cx)
121 }
122
123 pub fn clear(&self, window: &mut Window, cx: &mut App) {
124 self.editor()
125 .update(cx, |editor, cx| editor.clear(window, cx))
126 }
127
128 pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
129 self.editor()
130 .update(cx, |editor, cx| editor.set_text(text, 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 let settings = ThemeSettings::get_global(cx);
138 let theme_color = cx.theme().colors();
139
140 let mut style = InputFieldStyle {
141 text_color: theme_color.text,
142 background_color: theme_color.editor_background,
143 border_color: theme_color.border_variant,
144 };
145
146 if self.disabled {
147 style.text_color = theme_color.text_disabled;
148 style.background_color = theme_color.editor_background;
149 style.border_color = theme_color.border_disabled;
150 }
151
152 // if self.error_message.is_some() {
153 // style.text_color = cx.theme().status().error;
154 // style.border_color = cx.theme().status().error_border
155 // }
156
157 let text_style = TextStyle {
158 font_family: settings.ui_font.family.clone(),
159 font_features: settings.ui_font.features.clone(),
160 font_size: rems(0.875).into(),
161 font_weight: settings.buffer_font.weight,
162 font_style: FontStyle::Normal,
163 line_height: relative(1.2),
164 color: style.text_color,
165 ..Default::default()
166 };
167
168 let editor_style = EditorStyle {
169 background: theme_color.ghost_element_background,
170 local_player: cx.theme().players().local(),
171 syntax: cx.theme().syntax().clone(),
172 text: text_style,
173 ..Default::default()
174 };
175
176 let focus_handle = self.editor.focus_handle(cx);
177
178 let configured_handle = if let Some(tab_index) = self.tab_index {
179 focus_handle.tab_index(tab_index).tab_stop(self.tab_stop)
180 } else if !self.tab_stop {
181 focus_handle.tab_stop(false)
182 } else {
183 focus_handle
184 };
185
186 v_flex()
187 .id(self.placeholder.clone())
188 .w_full()
189 .gap_1()
190 .when_some(self.label.clone(), |this, label| {
191 this.child(
192 Label::new(label)
193 .size(self.label_size)
194 .color(if self.disabled {
195 Color::Disabled
196 } else {
197 Color::Default
198 }),
199 )
200 })
201 .child(
202 h_flex()
203 .track_focus(&configured_handle)
204 .min_w(self.min_width)
205 .min_h_8()
206 .w_full()
207 .px_2()
208 .py_1p5()
209 .flex_grow()
210 .text_color(style.text_color)
211 .rounded_md()
212 .bg(style.background_color)
213 .border_1()
214 .border_color(style.border_color)
215 .when(
216 editor.focus_handle(cx).contains_focused(window, cx),
217 |this| this.border_color(theme_color.border_focused),
218 )
219 .when_some(self.start_icon, |this, icon| {
220 this.gap_1()
221 .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
222 })
223 .child(EditorElement::new(&self.editor, editor_style)),
224 )
225 }
226}
227
228impl Component for InputField {
229 fn scope() -> ComponentScope {
230 ComponentScope::Input
231 }
232
233 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
234 let input_small =
235 cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label"));
236
237 let input_regular = cx.new(|cx| {
238 InputField::new(window, cx, "placeholder")
239 .label("Regular Label")
240 .label_size(LabelSize::Default)
241 });
242
243 Some(
244 v_flex()
245 .gap_6()
246 .children(vec![example_group(vec![
247 single_example(
248 "Small Label (Default)",
249 div().child(input_small).into_any_element(),
250 ),
251 single_example(
252 "Regular Label",
253 div().child(input_regular).into_any_element(),
254 ),
255 ])])
256 .into_any_element(),
257 )
258 }
259}