gpui_ui_kit/
select.rs

1//! Select/Dropdown component
2//!
3//! A dropdown select component for choosing from options with theming support.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Theme colors for select styling
9#[derive(Debug, Clone)]
10pub struct SelectTheme {
11    /// Trigger background color
12    pub trigger_bg: Rgba,
13    /// Trigger border color
14    pub trigger_border: Rgba,
15    /// Trigger border color on hover
16    pub trigger_border_hover: Rgba,
17    /// Trigger border color when focused/open
18    pub trigger_border_focused: Rgba,
19    /// Dropdown background color
20    pub dropdown_bg: Rgba,
21    /// Dropdown border color
22    pub dropdown_border: Rgba,
23    /// Selected option background
24    pub selected_bg: Rgba,
25    /// Option hover background
26    pub option_hover_bg: Rgba,
27    /// Label text color
28    pub label_color: Rgba,
29    /// Text color for selected value
30    pub text_color: Rgba,
31    /// Placeholder text color
32    pub placeholder_color: Rgba,
33    /// Option text color
34    pub option_text_color: Rgba,
35    /// Selected option text color
36    pub selected_text_color: Rgba,
37    /// Disabled text color
38    pub disabled_color: Rgba,
39    /// Arrow/chevron color
40    pub arrow_color: Rgba,
41}
42
43impl Default for SelectTheme {
44    fn default() -> Self {
45        Self {
46            trigger_bg: rgba(0x1e1e1eff),
47            trigger_border: rgba(0x3a3a3aff),
48            trigger_border_hover: rgba(0x007accff),
49            trigger_border_focused: rgba(0x007accff),
50            dropdown_bg: rgba(0x2a2a2aff),
51            dropdown_border: rgba(0x3a3a3aff),
52            selected_bg: rgba(0x007accff),
53            option_hover_bg: rgba(0x3a3a3aff),
54            label_color: rgba(0xccccccff),
55            text_color: rgba(0xffffffff),
56            placeholder_color: rgba(0x666666ff),
57            option_text_color: rgba(0xccccccff),
58            selected_text_color: rgba(0xffffffff),
59            disabled_color: rgba(0x666666ff),
60            arrow_color: rgba(0x666666ff),
61        }
62    }
63}
64
65/// Select size variants
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67pub enum SelectSize {
68    /// Small
69    Sm,
70    /// Medium (default)
71    #[default]
72    Md,
73    /// Large
74    Lg,
75}
76
77/// A select option
78#[derive(Clone)]
79pub struct SelectOption {
80    /// Option value
81    pub value: SharedString,
82    /// Display label
83    pub label: SharedString,
84    /// Whether option is disabled
85    pub disabled: bool,
86}
87
88impl SelectOption {
89    /// Create a new select option
90    pub fn new(value: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
91        Self {
92            value: value.into(),
93            label: label.into(),
94            disabled: false,
95        }
96    }
97
98    /// Set disabled state
99    pub fn disabled(mut self, disabled: bool) -> Self {
100        self.disabled = disabled;
101        self
102    }
103}
104
105/// A select dropdown component with theming support
106pub struct Select {
107    id: ElementId,
108    options: Vec<SelectOption>,
109    selected: Option<SharedString>,
110    placeholder: Option<SharedString>,
111    label: Option<SharedString>,
112    size: SelectSize,
113    disabled: bool,
114    is_open: bool,
115    theme: Option<SelectTheme>,
116    on_change: Option<Box<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>>,
117}
118
119impl Select {
120    /// Create a new select
121    pub fn new(id: impl Into<ElementId>) -> Self {
122        Self {
123            id: id.into(),
124            options: Vec::new(),
125            selected: None,
126            placeholder: None,
127            label: None,
128            size: SelectSize::default(),
129            disabled: false,
130            is_open: false,
131            theme: None,
132            on_change: None,
133        }
134    }
135
136    /// Set options
137    pub fn options(mut self, options: Vec<SelectOption>) -> Self {
138        self.options = options;
139        self
140    }
141
142    /// Set selected value
143    pub fn selected(mut self, value: impl Into<SharedString>) -> Self {
144        self.selected = Some(value.into());
145        self
146    }
147
148    /// Set placeholder
149    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
150        self.placeholder = Some(placeholder.into());
151        self
152    }
153
154    /// Set label
155    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
156        self.label = Some(label.into());
157        self
158    }
159
160    /// Set size
161    pub fn size(mut self, size: SelectSize) -> Self {
162        self.size = size;
163        self
164    }
165
166    /// Set disabled state
167    pub fn disabled(mut self, disabled: bool) -> Self {
168        self.disabled = disabled;
169        self
170    }
171
172    /// Set open state (for controlled component)
173    pub fn is_open(mut self, is_open: bool) -> Self {
174        self.is_open = is_open;
175        self
176    }
177
178    /// Set theme
179    pub fn theme(mut self, theme: SelectTheme) -> Self {
180        self.theme = Some(theme);
181        self
182    }
183
184    /// Set change handler
185    pub fn on_change(
186        mut self,
187        handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
188    ) -> Self {
189        self.on_change = Some(Box::new(handler));
190        self
191    }
192
193    /// Build into element
194    pub fn build(self) -> Div {
195        let default_theme = SelectTheme::default();
196        let theme = self.theme.as_ref().unwrap_or(&default_theme);
197
198        let (py, _text_size_class) = match self.size {
199            SelectSize::Sm => (px(4.0), "sm"),
200            SelectSize::Md => (px(8.0), "md"),
201            SelectSize::Lg => (px(12.0), "lg"),
202        };
203
204        let mut container = div().flex().flex_col().gap_1();
205
206        // Label
207        if let Some(label) = self.label {
208            container = container.child(
209                div()
210                    .text_sm()
211                    .text_color(theme.label_color)
212                    .font_weight(FontWeight::MEDIUM)
213                    .child(label),
214            );
215        }
216
217        // Find selected option label
218        let selected_label = self.selected.as_ref().and_then(|val| {
219            self.options
220                .iter()
221                .find(|o| &o.value == val)
222                .map(|o| o.label.clone())
223        });
224
225        // Select trigger
226        let border_color = if self.is_open {
227            theme.trigger_border_focused
228        } else {
229            theme.trigger_border
230        };
231
232        let mut trigger = div()
233            .id(self.id)
234            .flex()
235            .items_center()
236            .justify_between()
237            .px_3()
238            .py(py)
239            .min_w(px(120.0))
240            .bg(theme.trigger_bg)
241            .border_1()
242            .border_color(border_color)
243            .rounded_md()
244            .cursor_pointer();
245
246        // Apply text size
247        trigger = match self.size {
248            SelectSize::Sm => trigger.text_xs(),
249            SelectSize::Md => trigger.text_sm(),
250            SelectSize::Lg => trigger,
251        };
252
253        if self.disabled {
254            trigger = trigger.opacity(0.5).cursor_not_allowed();
255        } else {
256            let hover_border = theme.trigger_border_hover;
257            trigger = trigger.hover(move |s| s.border_color(hover_border));
258        }
259
260        // Display value or placeholder
261        let display_text = if let Some(label) = selected_label {
262            div().text_color(theme.text_color).child(label)
263        } else if let Some(placeholder) = self.placeholder {
264            div().text_color(theme.placeholder_color).child(placeholder)
265        } else {
266            div().text_color(theme.placeholder_color).child("Select...")
267        };
268
269        trigger = trigger.child(display_text);
270
271        // Dropdown arrow
272        trigger = trigger.child(div().text_xs().text_color(theme.arrow_color).child("▼"));
273
274        container = container.child(trigger);
275
276        // Dropdown menu (only shown when open)
277        if self.is_open {
278            let mut dropdown = div()
279                .id("select-dropdown")
280                .absolute()
281                .top_full()
282                .left_0()
283                .right_0()
284                .mt_1()
285                .bg(theme.dropdown_bg)
286                .border_1()
287                .border_color(theme.dropdown_border)
288                .rounded_md()
289                .shadow_lg()
290                .max_h(px(200.0))
291                .overflow_y_scroll()
292                .py_1();
293
294            for option in self.options {
295                let is_selected = self.selected.as_ref() == Some(&option.value);
296                let option_value = option.value.clone();
297
298                let mut option_el = div().px_3().py(px(6.0)).cursor_pointer();
299
300                // Apply text size
301                option_el = match self.size {
302                    SelectSize::Sm => option_el.text_xs(),
303                    SelectSize::Md => option_el.text_sm(),
304                    SelectSize::Lg => option_el,
305                };
306
307                if option.disabled {
308                    option_el = option_el
309                        .text_color(theme.disabled_color)
310                        .cursor_not_allowed();
311                } else if is_selected {
312                    option_el = option_el
313                        .bg(theme.selected_bg)
314                        .text_color(theme.selected_text_color);
315                } else {
316                    let hover_bg = theme.option_hover_bg;
317                    option_el = option_el
318                        .text_color(theme.option_text_color)
319                        .hover(move |s| s.bg(hover_bg));
320
321                    // Add click handler for non-disabled, non-selected options
322                    if let Some(ref handler) = self.on_change {
323                        let handler_ptr: *const dyn Fn(&SharedString, &mut Window, &mut App) =
324                            handler.as_ref() as *const _;
325                        option_el = option_el.on_mouse_up(
326                            MouseButton::Left,
327                            move |_event, window, cx| unsafe {
328                                (*handler_ptr)(&option_value, window, cx);
329                            },
330                        );
331                    }
332                }
333
334                option_el = option_el.child(option.label);
335                dropdown = dropdown.child(option_el);
336            }
337
338            container = container.relative().child(dropdown);
339        }
340
341        container
342    }
343}
344
345impl IntoElement for Select {
346    type Element = Div;
347
348    fn into_element(self) -> Self::Element {
349        self.build()
350    }
351}