gpui_ui_kit/
input.rs

1//! Input component
2//!
3//! Text input field with optional label, placeholder, and validation.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Input size variants
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum InputSize {
11    /// Small input
12    Sm,
13    /// Medium input (default)
14    #[default]
15    Md,
16    /// Large input
17    Lg,
18}
19
20/// Input visual variant
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum InputVariant {
23    /// Default input style
24    #[default]
25    Default,
26    /// Filled background
27    Filled,
28    /// Flushed (bottom border only)
29    Flushed,
30}
31
32/// A text input component
33pub struct Input {
34    id: ElementId,
35    value: SharedString,
36    placeholder: Option<SharedString>,
37    label: Option<SharedString>,
38    size: InputSize,
39    variant: InputVariant,
40    disabled: bool,
41    readonly: bool,
42    error: Option<SharedString>,
43    icon_left: Option<SharedString>,
44    icon_right: Option<SharedString>,
45}
46
47impl Input {
48    /// Create a new input
49    pub fn new(id: impl Into<ElementId>) -> Self {
50        Self {
51            id: id.into(),
52            value: "".into(),
53            placeholder: None,
54            label: None,
55            size: InputSize::default(),
56            variant: InputVariant::default(),
57            disabled: false,
58            readonly: false,
59            error: None,
60            icon_left: None,
61            icon_right: None,
62        }
63    }
64
65    /// Set the input value
66    pub fn value(mut self, value: impl Into<SharedString>) -> Self {
67        self.value = value.into();
68        self
69    }
70
71    /// Set placeholder text
72    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
73        self.placeholder = Some(placeholder.into());
74        self
75    }
76
77    /// Set label text
78    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
79        self.label = Some(label.into());
80        self
81    }
82
83    /// Set input size
84    pub fn size(mut self, size: InputSize) -> Self {
85        self.size = size;
86        self
87    }
88
89    /// Set input variant
90    pub fn variant(mut self, variant: InputVariant) -> Self {
91        self.variant = variant;
92        self
93    }
94
95    /// Set disabled state
96    pub fn disabled(mut self, disabled: bool) -> Self {
97        self.disabled = disabled;
98        self
99    }
100
101    /// Set readonly state
102    pub fn readonly(mut self, readonly: bool) -> Self {
103        self.readonly = readonly;
104        self
105    }
106
107    /// Set error message
108    pub fn error(mut self, error: impl Into<SharedString>) -> Self {
109        self.error = Some(error.into());
110        self
111    }
112
113    /// Set left icon
114    pub fn icon_left(mut self, icon: impl Into<SharedString>) -> Self {
115        self.icon_left = Some(icon.into());
116        self
117    }
118
119    /// Set right icon
120    pub fn icon_right(mut self, icon: impl Into<SharedString>) -> Self {
121        self.icon_right = Some(icon.into());
122        self
123    }
124
125    /// Build into element
126    pub fn build(self) -> Div {
127        let (py, text_size) = match self.size {
128            InputSize::Sm => (px(4.0), "text_xs"),
129            InputSize::Md => (px(8.0), "text_sm"),
130            InputSize::Lg => (px(12.0), "text_base"),
131        };
132
133        let has_error = self.error.is_some();
134        let border_color = if has_error {
135            rgb(0xcc3333)
136        } else {
137            rgb(0x3a3a3a)
138        };
139
140        let mut container = div().flex().flex_col().gap_1();
141
142        // Label
143        if let Some(label) = self.label {
144            container = container.child(
145                div()
146                    .text_sm()
147                    .text_color(rgb(0xcccccc))
148                    .font_weight(FontWeight::MEDIUM)
149                    .child(label),
150            );
151        }
152
153        // Input wrapper
154        let mut input_wrapper = div()
155            .id(self.id)
156            .flex()
157            .items_center()
158            .gap_2()
159            .px_3()
160            .py(py)
161            .rounded_md()
162            .border_1()
163            .border_color(border_color);
164
165        // Apply variant styling
166        match self.variant {
167            InputVariant::Default => {
168                input_wrapper = input_wrapper.bg(rgb(0x1e1e1e));
169            }
170            InputVariant::Filled => {
171                input_wrapper = input_wrapper
172                    .bg(rgb(0x2a2a2a))
173                    .border_color(rgba(0x00000000));
174            }
175            InputVariant::Flushed => {
176                input_wrapper = input_wrapper
177                    .bg(rgba(0x00000000))
178                    .border_0()
179                    .border_b_1()
180                    .border_color(border_color)
181                    .rounded_none();
182            }
183        }
184
185        if self.disabled {
186            input_wrapper = input_wrapper.opacity(0.5).cursor_not_allowed();
187        } else if !self.readonly {
188            input_wrapper = input_wrapper.hover(|s| s.border_color(rgb(0x007acc)));
189        }
190
191        // Left icon
192        if let Some(icon) = self.icon_left {
193            input_wrapper = input_wrapper.child(div().text_color(rgb(0x666666)).child(icon));
194        }
195
196        // Input text/placeholder
197        let text_el = if self.value.is_empty() {
198            if let Some(placeholder) = self.placeholder {
199                div().flex_1().text_color(rgb(0x666666)).child(placeholder)
200            } else {
201                div().flex_1()
202            }
203        } else {
204            div().flex_1().text_color(rgb(0xffffff)).child(self.value)
205        };
206
207        // Apply text size
208        let text_el = match self.size {
209            InputSize::Sm => text_el.text_xs(),
210            InputSize::Md => text_el.text_sm(),
211            InputSize::Lg => text_el,
212        };
213
214        input_wrapper = input_wrapper.child(text_el);
215
216        // Right icon
217        if let Some(icon) = self.icon_right {
218            input_wrapper = input_wrapper.child(div().text_color(rgb(0x666666)).child(icon));
219        }
220
221        container = container.child(input_wrapper);
222
223        // Error message
224        if let Some(error) = self.error {
225            container = container.child(div().text_xs().text_color(rgb(0xcc3333)).child(error));
226        }
227
228        container
229    }
230}
231
232impl IntoElement for Input {
233    type Element = Div;
234
235    fn into_element(self) -> Self::Element {
236        self.build()
237    }
238}