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