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 editor::*;
9use gpui::*;
10use settings::Settings;
11use theme::ThemeSettings;
12use ui::*;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum FieldLabelLayout {
16 Inline,
17 Stacked,
18}
19
20pub struct TextFieldStyle {
21 text_color: Hsla,
22 background_color: Hsla,
23 border_color: Hsla,
24}
25
26/// A Text Field view that can be used to create text fields like search inputs, form fields, etc.
27///
28/// It wraps a single line [`Editor`] view and allows for common field properties like labels, placeholders, icons, etc.
29pub struct TextField {
30 /// An optional label for the text field.
31 ///
32 /// Its position is determined by the [`FieldLabelLayout`].
33 label: Option<SharedString>,
34 /// The placeholder text for the text field.
35 ///
36 /// All text fields must have placeholder text that is displayed when the field is empty.
37 placeholder: SharedString,
38 /// Exposes the underlying [`View<Editor>`] to allow for customizing the editor beyond the provided API.
39 ///
40 /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases.
41 pub editor: View<Editor>,
42 /// An optional icon that is displayed at the start of the text field.
43 ///
44 /// For example, a magnifying glass icon in a search field.
45 start_icon: Option<IconName>,
46 /// The layout of the label relative to the text field.
47 label_layout: FieldLabelLayout,
48}
49
50impl FocusableView for TextField {
51 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
52 self.editor.focus_handle(cx)
53 }
54}
55
56impl TextField {
57 pub fn new(placeholder: impl Into<SharedString>, cx: &mut WindowContext) -> Self {
58 let placeholder_text = placeholder.into();
59
60 let editor = cx.new_view(|cx| {
61 let mut input = Editor::single_line(cx);
62 input.set_placeholder_text(placeholder_text.clone(), cx);
63 input
64 });
65
66 Self {
67 label: None,
68 placeholder: placeholder_text,
69 editor,
70 start_icon: None,
71 label_layout: FieldLabelLayout::Stacked,
72 }
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 placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
81 self.placeholder = placeholder.into();
82 self
83 }
84
85 pub fn start_icon(mut self, icon: IconName) -> Self {
86 self.start_icon = Some(icon);
87 self
88 }
89
90 pub fn label_layout(mut self, layout: FieldLabelLayout) -> Self {
91 self.label_layout = layout;
92 self
93 }
94}
95
96impl Render for TextField {
97 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
98 let settings = ThemeSettings::get_global(cx);
99 let theme_color = cx.theme().colors();
100
101 let style = TextFieldStyle {
102 text_color: theme_color.text,
103 background_color: theme_color.ghost_element_background,
104 border_color: theme_color.border,
105 };
106
107 // if self.disabled {
108 // style.text_color = theme_color.text_disabled;
109 // style.background_color = theme_color.ghost_element_disabled;
110 // style.border_color = theme_color.border_disabled;
111 // }
112
113 // if self.error_message.is_some() {
114 // style.text_color = cx.theme().status().error;
115 // style.border_color = cx.theme().status().error_border
116 // }
117
118 let text_style = TextStyle {
119 font_family: settings.buffer_font.family.clone(),
120 font_features: settings.buffer_font.features,
121 font_size: rems(0.875).into(),
122 font_weight: FontWeight::NORMAL,
123 font_style: FontStyle::Normal,
124 line_height: relative(1.2),
125 color: style.text_color,
126 ..Default::default()
127 };
128
129 let editor_style = EditorStyle {
130 background: theme_color.ghost_element_background,
131 local_player: cx.theme().players().local(),
132 text: text_style,
133 ..Default::default()
134 };
135
136 let stacked_label: Option<Label> = if self.label_layout == FieldLabelLayout::Stacked {
137 self.label
138 .clone()
139 .map(|label| Label::new(label).size(LabelSize::Small))
140 } else {
141 None
142 };
143
144 let inline_label: Option<Label> = if self.label_layout == FieldLabelLayout::Inline {
145 self.label
146 .clone()
147 .map(|label| Label::new(label).size(LabelSize::Small))
148 } else {
149 None
150 };
151
152 div()
153 .when_some(stacked_label, |this, label| this.child(label))
154 .child(
155 v_flex()
156 .w_full()
157 .px_2()
158 .py_1()
159 .bg(style.background_color)
160 .text_color(style.text_color)
161 .rounded_lg()
162 .border()
163 .border_color(style.border_color)
164 .w_48()
165 .child(
166 h_flex()
167 .gap_2()
168 .when_some(inline_label, |this, label| this.child(label))
169 .child(
170 h_flex()
171 .gap_1()
172 .when_some(self.start_icon, |this, icon| {
173 this.child(
174 Icon::new(icon)
175 .size(IconSize::Small)
176 .color(Color::Muted),
177 )
178 })
179 .child(EditorElement::new(&self.editor, editor_style)),
180 ),
181 ),
182 )
183 }
184}