input_field.rs

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