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