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