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}
41
42impl Focusable for InputField {
43 fn focus_handle(&self, cx: &App) -> FocusHandle {
44 self.editor.focus_handle(cx)
45 }
46}
47
48impl InputField {
49 pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into<SharedString>) -> Self {
50 let placeholder_text = placeholder.into();
51
52 let editor = cx.new(|cx| {
53 let mut input = Editor::single_line(window, cx);
54 input.set_placeholder_text(&placeholder_text, window, cx);
55 input
56 });
57
58 Self {
59 label: None,
60 label_size: LabelSize::Small,
61 placeholder: placeholder_text,
62 editor,
63 start_icon: None,
64 disabled: false,
65 min_width: px(192.).into(),
66 }
67 }
68
69 pub fn start_icon(mut self, icon: IconName) -> Self {
70 self.start_icon = Some(icon);
71 self
72 }
73
74 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
75 self.label = Some(label.into());
76 self
77 }
78
79 pub fn label_size(mut self, size: LabelSize) -> Self {
80 self.label_size = size;
81 self
82 }
83
84 pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
85 self.min_width = width.into();
86 self
87 }
88
89 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
90 self.disabled = disabled;
91 self.editor
92 .update(cx, |editor, _| editor.set_read_only(disabled))
93 }
94
95 pub fn is_empty(&self, cx: &App) -> bool {
96 self.editor().read(cx).text(cx).trim().is_empty()
97 }
98
99 pub fn editor(&self) -> &Entity<Editor> {
100 &self.editor
101 }
102
103 pub fn text(&self, cx: &App) -> String {
104 self.editor().read(cx).text(cx)
105 }
106
107 pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
108 self.editor()
109 .update(cx, |editor, cx| editor.set_text(text, window, cx))
110 }
111}
112
113impl Render for InputField {
114 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
115 let settings = ThemeSettings::get_global(cx);
116 let theme_color = cx.theme().colors();
117
118 let mut style = InputFieldStyle {
119 text_color: theme_color.text,
120 background_color: theme_color.editor_background,
121 border_color: theme_color.border_variant,
122 };
123
124 if self.disabled {
125 style.text_color = theme_color.text_disabled;
126 style.background_color = theme_color.editor_background;
127 style.border_color = theme_color.border_disabled;
128 }
129
130 // if self.error_message.is_some() {
131 // style.text_color = cx.theme().status().error;
132 // style.border_color = cx.theme().status().error_border
133 // }
134
135 let text_style = TextStyle {
136 font_family: settings.ui_font.family.clone(),
137 font_features: settings.ui_font.features.clone(),
138 font_size: rems(0.875).into(),
139 font_weight: settings.buffer_font.weight,
140 font_style: FontStyle::Normal,
141 line_height: relative(1.2),
142 color: style.text_color,
143 ..Default::default()
144 };
145
146 let editor_style = EditorStyle {
147 background: theme_color.ghost_element_background,
148 local_player: cx.theme().players().local(),
149 syntax: cx.theme().syntax().clone(),
150 text: text_style,
151 ..Default::default()
152 };
153
154 v_flex()
155 .id(self.placeholder.clone())
156 .w_full()
157 .gap_1()
158 .when_some(self.label.clone(), |this, label| {
159 this.child(
160 Label::new(label)
161 .size(self.label_size)
162 .color(if self.disabled {
163 Color::Disabled
164 } else {
165 Color::Default
166 }),
167 )
168 })
169 .child(
170 h_flex()
171 .min_w(self.min_width)
172 .min_h_8()
173 .w_full()
174 .px_2()
175 .py_1p5()
176 .flex_grow()
177 .text_color(style.text_color)
178 .rounded_md()
179 .bg(style.background_color)
180 .border_1()
181 .border_color(style.border_color)
182 .when_some(self.start_icon, |this, icon| {
183 this.gap_1()
184 .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
185 })
186 .child(EditorElement::new(&self.editor, editor_style)),
187 )
188 }
189}
190
191impl Component for InputField {
192 fn scope() -> ComponentScope {
193 ComponentScope::Input
194 }
195
196 fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
197 let input_small =
198 cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label"));
199
200 let input_regular = cx.new(|cx| {
201 InputField::new(window, cx, "placeholder")
202 .label("Regular Label")
203 .label_size(LabelSize::Default)
204 });
205
206 Some(
207 v_flex()
208 .gap_6()
209 .children(vec![example_group(vec![
210 single_example(
211 "Small Label (Default)",
212 div().child(input_small).into_any_element(),
213 ),
214 single_example(
215 "Regular Label",
216 div().child(input_regular).into_any_element(),
217 ),
218 ])])
219 .into_any_element(),
220 )
221 }
222}