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