ui_text_field.rs

  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}