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