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