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::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, TextStyle};
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 that can be used to create text fields like search inputs, form fields, etc.
28///
29/// It wraps a single line [`Editor`] 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 [`Model<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: Entity<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 Focusable for TextField {
52 fn focus_handle(&self, cx: &App) -> FocusHandle {
53 self.editor.focus_handle(cx)
54 }
55}
56
57impl TextField {
58 pub fn new(
59 window: &mut Window,
60 cx: &mut App,
61 label: impl Into<SharedString>,
62 placeholder: impl Into<SharedString>,
63 ) -> Self {
64 let placeholder_text = placeholder.into();
65
66 let editor = cx.new(|cx| {
67 let mut input = Editor::single_line(window, cx);
68 input.set_placeholder_text(placeholder_text.clone(), cx);
69 input
70 });
71
72 Self {
73 label: label.into(),
74 placeholder: placeholder_text,
75 editor,
76 start_icon: None,
77 with_label: FieldLabelLayout::Hidden,
78 disabled: false,
79 }
80 }
81
82 pub fn start_icon(mut self, icon: IconName) -> Self {
83 self.start_icon = Some(icon);
84 self
85 }
86
87 pub fn with_label(mut self, layout: FieldLabelLayout) -> Self {
88 self.with_label = layout;
89 self
90 }
91
92 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
93 self.disabled = disabled;
94 self.editor
95 .update(cx, |editor, _| editor.set_read_only(disabled))
96 }
97
98 pub fn editor(&self) -> &Entity<Editor> {
99 &self.editor
100 }
101}
102
103impl Render for TextField {
104 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
105 let settings = ThemeSettings::get_global(cx);
106 let theme_color = cx.theme().colors();
107
108 let mut style = TextFieldStyle {
109 text_color: theme_color.text,
110 background_color: theme_color.ghost_element_background,
111 border_color: theme_color.border,
112 };
113
114 if self.disabled {
115 style.text_color = theme_color.text_disabled;
116 style.background_color = theme_color.ghost_element_disabled;
117 style.border_color = theme_color.border_disabled;
118 }
119
120 // if self.error_message.is_some() {
121 // style.text_color = cx.theme().status().error;
122 // style.border_color = cx.theme().status().error_border
123 // }
124
125 let text_style = TextStyle {
126 font_family: settings.buffer_font.family.clone(),
127 font_features: settings.buffer_font.features.clone(),
128 font_size: rems(0.875).into(),
129 font_weight: settings.buffer_font.weight,
130 font_style: FontStyle::Normal,
131 line_height: relative(1.2),
132 color: style.text_color,
133 ..Default::default()
134 };
135
136 let editor_style = EditorStyle {
137 background: theme_color.ghost_element_background,
138 local_player: cx.theme().players().local(),
139 text: text_style,
140 ..Default::default()
141 };
142
143 div()
144 .id(self.placeholder.clone())
145 .group("text-field")
146 .w_full()
147 .when(self.with_label == FieldLabelLayout::Stacked, |this| {
148 this.child(
149 Label::new(self.label.clone())
150 .size(LabelSize::Default)
151 .color(if self.disabled {
152 Color::Disabled
153 } else {
154 Color::Muted
155 }),
156 )
157 })
158 .child(
159 v_flex().w_full().child(
160 h_flex()
161 .w_full()
162 .flex_grow()
163 .gap_2()
164 .when(self.with_label == FieldLabelLayout::Inline, |this| {
165 this.child(Label::new(self.label.clone()).size(LabelSize::Default))
166 })
167 .child(
168 h_flex()
169 .px_2()
170 .py_1()
171 .bg(style.background_color)
172 .text_color(style.text_color)
173 .rounded_lg()
174 .border_1()
175 .border_color(style.border_color)
176 .min_w_48()
177 .w_full()
178 .flex_grow()
179 .gap_1()
180 .when_some(self.start_icon, |this, icon| {
181 this.child(
182 Icon::new(icon).size(IconSize::Small).color(Color::Muted),
183 )
184 })
185 .child(EditorElement::new(&self.editor, editor_style)),
186 ),
187 ),
188 )
189 }
190}