input_field.rs

  1use component::{example_group, single_example};
  2
  3use gpui::{App, FocusHandle, Focusable, Hsla, Length};
  4use std::sync::Arc;
  5
  6use ui::prelude::*;
  7
  8use crate::ErasedEditor;
  9
 10pub struct InputFieldStyle {
 11    text_color: Hsla,
 12    background_color: Hsla,
 13    border_color: Hsla,
 14}
 15
 16/// An Input Field component that can be used to create text fields like search inputs, form fields, etc.
 17///
 18/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc.
 19#[derive(RegisterComponent)]
 20pub struct InputField {
 21    /// An optional label for the text field.
 22    ///
 23    /// Its position is determined by the [`FieldLabelLayout`].
 24    label: Option<SharedString>,
 25    /// The size of the label text.
 26    label_size: LabelSize,
 27    /// The placeholder text for the text field.
 28    placeholder: SharedString,
 29
 30    editor: Arc<dyn ErasedEditor>,
 31    /// An optional icon that is displayed at the start of the text field.
 32    ///
 33    /// For example, a magnifying glass icon in a search field.
 34    start_icon: Option<IconName>,
 35    /// The minimum width of for the input
 36    min_width: Length,
 37    /// The tab index for keyboard navigation order.
 38    tab_index: Option<isize>,
 39    /// Whether this field is a tab stop (can be focused via Tab key).
 40    tab_stop: bool,
 41}
 42
 43impl Focusable for InputField {
 44    fn focus_handle(&self, cx: &App) -> FocusHandle {
 45        self.editor.focus_handle(cx)
 46    }
 47}
 48
 49impl InputField {
 50    pub fn new(window: &mut Window, cx: &mut App, placeholder_text: &str) -> Self {
 51        let editor_factory = crate::ERASED_EDITOR_FACTORY
 52            .get()
 53            .expect("ErasedEditorFactory to be initialized");
 54        let editor = (editor_factory)(window, cx);
 55        editor.set_placeholder_text(placeholder_text, window, cx);
 56
 57        Self {
 58            label: None,
 59            label_size: LabelSize::Small,
 60            placeholder: SharedString::new(placeholder_text),
 61            editor,
 62            start_icon: None,
 63            min_width: px(192.).into(),
 64            tab_index: None,
 65            tab_stop: true,
 66        }
 67    }
 68
 69    pub fn start_icon(mut self, icon: IconName) -> Self {
 70        self.start_icon = Some(icon);
 71        self
 72    }
 73
 74    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
 75        self.label = Some(label.into());
 76        self
 77    }
 78
 79    pub fn label_size(mut self, size: LabelSize) -> Self {
 80        self.label_size = size;
 81        self
 82    }
 83
 84    pub fn label_min_width(mut self, width: impl Into<Length>) -> Self {
 85        self.min_width = width.into();
 86        self
 87    }
 88
 89    pub fn tab_index(mut self, index: isize) -> Self {
 90        self.tab_index = Some(index);
 91        self
 92    }
 93
 94    pub fn tab_stop(mut self, tab_stop: bool) -> Self {
 95        self.tab_stop = tab_stop;
 96        self
 97    }
 98
 99    pub fn is_empty(&self, cx: &App) -> bool {
100        self.editor().text(cx).trim().is_empty()
101    }
102
103    pub fn editor(&self) -> &Arc<dyn ErasedEditor> {
104        &self.editor
105    }
106
107    pub fn text(&self, cx: &App) -> String {
108        self.editor().text(cx)
109    }
110
111    pub fn clear(&self, window: &mut Window, cx: &mut App) {
112        self.editor().clear(window, cx)
113    }
114
115    pub fn set_text(&self, text: &str, window: &mut Window, cx: &mut App) {
116        self.editor().set_text(text, window, cx)
117    }
118}
119
120impl Render for InputField {
121    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
122        let editor = self.editor.clone();
123
124        let theme_color = cx.theme().colors();
125
126        let style = InputFieldStyle {
127            text_color: theme_color.text,
128            background_color: theme_color.editor_background,
129            border_color: theme_color.border_variant,
130        };
131
132        let focus_handle = self.editor.focus_handle(cx);
133
134        let configured_handle = if let Some(tab_index) = self.tab_index {
135            focus_handle.tab_index(tab_index).tab_stop(self.tab_stop)
136        } else if !self.tab_stop {
137            focus_handle.tab_stop(false)
138        } else {
139            focus_handle
140        };
141
142        v_flex()
143            .id(self.placeholder.clone())
144            .w_full()
145            .gap_1()
146            .when_some(self.label.clone(), |this, label| {
147                this.child(
148                    Label::new(label)
149                        .size(self.label_size)
150                        .color(Color::Default),
151                )
152            })
153            .child(
154                h_flex()
155                    .track_focus(&configured_handle)
156                    .min_w(self.min_width)
157                    .min_h_8()
158                    .w_full()
159                    .px_2()
160                    .py_1p5()
161                    .flex_grow()
162                    .text_color(style.text_color)
163                    .rounded_md()
164                    .bg(style.background_color)
165                    .border_1()
166                    .border_color(style.border_color)
167                    .when(
168                        editor.focus_handle(cx).contains_focused(window, cx),
169                        |this| this.border_color(theme_color.border_focused),
170                    )
171                    .when_some(self.start_icon, |this, icon| {
172                        this.gap_1()
173                            .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
174                    })
175                    .child(self.editor.render(window, cx)),
176            )
177    }
178}
179
180impl Component for InputField {
181    fn scope() -> ComponentScope {
182        ComponentScope::Input
183    }
184
185    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
186        let input_small =
187            cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label"));
188
189        let input_regular = cx.new(|cx| {
190            InputField::new(window, cx, "placeholder")
191                .label("Regular Label")
192                .label_size(LabelSize::Default)
193        });
194
195        Some(
196            v_flex()
197                .gap_6()
198                .children(vec![example_group(vec![
199                    single_example(
200                        "Small Label (Default)",
201                        div().child(input_small).into_any_element(),
202                    ),
203                    single_example(
204                        "Regular Label",
205                        div().child(input_regular).into_any_element(),
206                    ),
207                ])])
208                .into_any_element(),
209        )
210    }
211}