input_field.rs

  1use component::{example_group, single_example};
  2use editor::{Editor, EditorElement, EditorStyle};
  3use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle};
  4use settings::Settings;
  5use std::sync::Arc;
  6use theme::ThemeSettings;
  7use ui::prelude::*;
  8
  9pub struct InputFieldStyle {
 10    text_color: Hsla,
 11    background_color: Hsla,
 12    border_color: Hsla,
 13}
 14
 15/// An Input Field component that can be used to create text fields like search inputs, form fields, etc.
 16///
 17/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc.
 18#[derive(RegisterComponent)]
 19pub struct InputField {
 20    /// An optional label for the text field.
 21    ///
 22    /// Its position is determined by the [`FieldLabelLayout`].
 23    label: Option<SharedString>,
 24    /// The size of the label text.
 25    label_size: LabelSize,
 26    /// The placeholder text for the text field.
 27    placeholder: SharedString,
 28    /// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API.
 29    ///
 30    /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases.
 31    pub editor: Entity<Editor>,
 32    /// An optional icon that is displayed at the start of the text field.
 33    ///
 34    /// For example, a magnifying glass icon in a search field.
 35    start_icon: Option<IconName>,
 36    /// Whether the text field is disabled.
 37    disabled: bool,
 38    /// The minimum width of for the input
 39    min_width: Length,
 40    /// The tab index for keyboard navigation order.
 41    tab_index: Option<isize>,
 42    /// Whether this field is a tab stop (can be focused via Tab key).
 43    tab_stop: bool,
 44}
 45
 46impl Focusable for InputField {
 47    fn focus_handle(&self, cx: &App) -> FocusHandle {
 48        self.editor.focus_handle(cx)
 49    }
 50}
 51
 52impl InputField {
 53    pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into<SharedString>) -> Self {
 54        let placeholder_text = placeholder.into();
 55
 56        let editor = cx.new(|cx| {
 57            let mut input = Editor::single_line(window, cx);
 58            input.set_placeholder_text(&placeholder_text, window, cx);
 59            input
 60        });
 61
 62        Self {
 63            label: None,
 64            label_size: LabelSize::Small,
 65            placeholder: placeholder_text,
 66            editor,
 67            start_icon: None,
 68            disabled: false,
 69            min_width: px(192.).into(),
 70            tab_index: None,
 71            tab_stop: true,
 72        }
 73    }
 74
 75    pub fn start_icon(mut self, icon: IconName) -> Self {
 76        self.start_icon = Some(icon);
 77        self
 78    }
 79
 80    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
 81        self.label = Some(label.into());
 82        self
 83    }
 84
 85    pub fn label_size(mut self, size: LabelSize) -> Self {
 86        self.label_size = size;
 87        self
 88    }
 89
 90    pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
 91        self.min_width = width.into();
 92        self
 93    }
 94
 95    pub fn tab_index(mut self, index: isize) -> Self {
 96        self.tab_index = Some(index);
 97        self
 98    }
 99
100    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
101        self.tab_stop = tab_stop;
102        self
103    }
104
105    pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
106        self.disabled = disabled;
107        self.editor
108            .update(cx, |editor, _| editor.set_read_only(disabled))
109    }
110
111    pub fn is_empty(&self, cx: &App) -> bool {
112        self.editor().read(cx).text(cx).trim().is_empty()
113    }
114
115    pub fn editor(&self) -> &Entity<Editor> {
116        &self.editor
117    }
118
119    pub fn text(&self, cx: &App) -> String {
120        self.editor().read(cx).text(cx)
121    }
122
123    pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
124        self.editor()
125            .update(cx, |editor, cx| editor.set_text(text, window, cx))
126    }
127}
128
129impl Render for InputField {
130    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
131        let settings = ThemeSettings::get_global(cx);
132        let theme_color = cx.theme().colors();
133
134        let mut style = InputFieldStyle {
135            text_color: theme_color.text,
136            background_color: theme_color.editor_background,
137            border_color: theme_color.border_variant,
138        };
139
140        if self.disabled {
141            style.text_color = theme_color.text_disabled;
142            style.background_color = theme_color.editor_background;
143            style.border_color = theme_color.border_disabled;
144        }
145
146        // if self.error_message.is_some() {
147        //     style.text_color = cx.theme().status().error;
148        //     style.border_color = cx.theme().status().error_border
149        // }
150
151        let text_style = TextStyle {
152            font_family: settings.ui_font.family.clone(),
153            font_features: settings.ui_font.features.clone(),
154            font_size: rems(0.875).into(),
155            font_weight: settings.buffer_font.weight,
156            font_style: FontStyle::Normal,
157            line_height: relative(1.2),
158            color: style.text_color,
159            ..Default::default()
160        };
161
162        let editor_style = EditorStyle {
163            background: theme_color.ghost_element_background,
164            local_player: cx.theme().players().local(),
165            syntax: cx.theme().syntax().clone(),
166            text: text_style,
167            ..Default::default()
168        };
169
170        let focus_handle = self.editor.focus_handle(cx);
171
172        let configured_handle = if let Some(tab_index) = self.tab_index {
173            focus_handle.tab_index(tab_index).tab_stop(self.tab_stop)
174        } else if !self.tab_stop {
175            focus_handle.tab_stop(false)
176        } else {
177            focus_handle
178        };
179
180        v_flex()
181            .id(self.placeholder.clone())
182            .w_full()
183            .gap_1()
184            .when_some(self.label.clone(), |this, label| {
185                this.child(
186                    Label::new(label)
187                        .size(self.label_size)
188                        .color(if self.disabled {
189                            Color::Disabled
190                        } else {
191                            Color::Default
192                        }),
193                )
194            })
195            .child(
196                h_flex()
197                    .track_focus(&configured_handle)
198                    .min_w(self.min_width)
199                    .min_h_8()
200                    .w_full()
201                    .px_2()
202                    .py_1p5()
203                    .flex_grow()
204                    .text_color(style.text_color)
205                    .rounded_md()
206                    .bg(style.background_color)
207                    .border_1()
208                    .border_color(style.border_color)
209                    .when_some(self.start_icon, |this, icon| {
210                        this.gap_1()
211                            .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
212                    })
213                    .child(EditorElement::new(&self.editor, editor_style)),
214            )
215    }
216}
217
218impl Component for InputField {
219    fn scope() -> ComponentScope {
220        ComponentScope::Input
221    }
222
223    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
224        let input_small =
225            cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label"));
226
227        let input_regular = cx.new(|cx| {
228            InputField::new(window, cx, "placeholder")
229                .label("Regular Label")
230                .label_size(LabelSize::Default)
231        });
232
233        Some(
234            v_flex()
235                .gap_6()
236                .children(vec![example_group(vec![
237                    single_example(
238                        "Small Label (Default)",
239                        div().child(input_small).into_any_element(),
240                    ),
241                    single_example(
242                        "Regular Label",
243                        div().child(input_regular).into_any_element(),
244                    ),
245                ])])
246                .into_any_element(),
247        )
248    }
249}