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}
 41
 42impl Focusable for InputField {
 43    fn focus_handle(&self, cx: &App) -> FocusHandle {
 44        self.editor.focus_handle(cx)
 45    }
 46}
 47
 48impl InputField {
 49    pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into<SharedString>) -> Self {
 50        let placeholder_text = placeholder.into();
 51
 52        let editor = cx.new(|cx| {
 53            let mut input = Editor::single_line(window, cx);
 54            input.set_placeholder_text(&placeholder_text, window, cx);
 55            input
 56        });
 57
 58        Self {
 59            label: None,
 60            label_size: LabelSize::Small,
 61            placeholder: placeholder_text,
 62            editor,
 63            start_icon: None,
 64            disabled: false,
 65            min_width: px(192.).into(),
 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 set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
 90        self.disabled = disabled;
 91        self.editor
 92            .update(cx, |editor, _| editor.set_read_only(disabled))
 93    }
 94
 95    pub fn is_empty(&self, cx: &App) -> bool {
 96        self.editor().read(cx).text(cx).trim().is_empty()
 97    }
 98
 99    pub fn editor(&self) -> &Entity<Editor> {
100        &self.editor
101    }
102
103    pub fn text(&self, cx: &App) -> String {
104        self.editor().read(cx).text(cx)
105    }
106
107    pub fn set_text(&self, text: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
108        self.editor()
109            .update(cx, |editor, cx| editor.set_text(text, window, cx))
110    }
111}
112
113impl Render for InputField {
114    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
115        let settings = ThemeSettings::get_global(cx);
116        let theme_color = cx.theme().colors();
117
118        let mut style = InputFieldStyle {
119            text_color: theme_color.text,
120            background_color: theme_color.editor_background,
121            border_color: theme_color.border_variant,
122        };
123
124        if self.disabled {
125            style.text_color = theme_color.text_disabled;
126            style.background_color = theme_color.editor_background;
127            style.border_color = theme_color.border_disabled;
128        }
129
130        // if self.error_message.is_some() {
131        //     style.text_color = cx.theme().status().error;
132        //     style.border_color = cx.theme().status().error_border
133        // }
134
135        let text_style = TextStyle {
136            font_family: settings.ui_font.family.clone(),
137            font_features: settings.ui_font.features.clone(),
138            font_size: rems(0.875).into(),
139            font_weight: settings.buffer_font.weight,
140            font_style: FontStyle::Normal,
141            line_height: relative(1.2),
142            color: style.text_color,
143            ..Default::default()
144        };
145
146        let editor_style = EditorStyle {
147            background: theme_color.ghost_element_background,
148            local_player: cx.theme().players().local(),
149            syntax: cx.theme().syntax().clone(),
150            text: text_style,
151            ..Default::default()
152        };
153
154        v_flex()
155            .id(self.placeholder.clone())
156            .w_full()
157            .gap_1()
158            .when_some(self.label.clone(), |this, label| {
159                this.child(
160                    Label::new(label)
161                        .size(self.label_size)
162                        .color(if self.disabled {
163                            Color::Disabled
164                        } else {
165                            Color::Default
166                        }),
167                )
168            })
169            .child(
170                h_flex()
171                    .min_w(self.min_width)
172                    .min_h_8()
173                    .w_full()
174                    .px_2()
175                    .py_1p5()
176                    .flex_grow()
177                    .text_color(style.text_color)
178                    .rounded_md()
179                    .bg(style.background_color)
180                    .border_1()
181                    .border_color(style.border_color)
182                    .when_some(self.start_icon, |this, icon| {
183                        this.gap_1()
184                            .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
185                    })
186                    .child(EditorElement::new(&self.editor, editor_style)),
187            )
188    }
189}
190
191impl Component for InputField {
192    fn scope() -> ComponentScope {
193        ComponentScope::Input
194    }
195
196    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
197        let input_small =
198            cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label"));
199
200        let input_regular = cx.new(|cx| {
201            InputField::new(window, cx, "placeholder")
202                .label("Regular Label")
203                .label_size(LabelSize::Default)
204        });
205
206        Some(
207            v_flex()
208                .gap_6()
209                .children(vec![example_group(vec![
210                    single_example(
211                        "Small Label (Default)",
212                        div().child(input_small).into_any_element(),
213                    ),
214                    single_example(
215                        "Regular Label",
216                        div().child(input_regular).into_any_element(),
217                    ),
218                ])])
219                .into_any_element(),
220        )
221    }
222}