gpui_ui_kit/
select.rs

1//! Select/Dropdown component
2//!
3//! A dropdown select component for choosing from options.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Select size variants
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum SelectSize {
11    /// Small
12    Sm,
13    /// Medium (default)
14    #[default]
15    Md,
16    /// Large
17    Lg,
18}
19
20/// A select option
21#[derive(Clone)]
22pub struct SelectOption {
23    /// Option value
24    pub value: SharedString,
25    /// Display label
26    pub label: SharedString,
27    /// Whether option is disabled
28    pub disabled: bool,
29}
30
31impl SelectOption {
32    /// Create a new select option
33    pub fn new(value: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
34        Self {
35            value: value.into(),
36            label: label.into(),
37            disabled: false,
38        }
39    }
40
41    /// Set disabled state
42    pub fn disabled(mut self, disabled: bool) -> Self {
43        self.disabled = disabled;
44        self
45    }
46}
47
48/// A select dropdown component
49pub struct Select {
50    id: ElementId,
51    options: Vec<SelectOption>,
52    selected: Option<SharedString>,
53    placeholder: Option<SharedString>,
54    label: Option<SharedString>,
55    size: SelectSize,
56    disabled: bool,
57    is_open: bool,
58    on_change: Option<Box<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>>,
59}
60
61impl Select {
62    /// Create a new select
63    pub fn new(id: impl Into<ElementId>) -> Self {
64        Self {
65            id: id.into(),
66            options: Vec::new(),
67            selected: None,
68            placeholder: None,
69            label: None,
70            size: SelectSize::default(),
71            disabled: false,
72            is_open: false,
73            on_change: None,
74        }
75    }
76
77    /// Set options
78    pub fn options(mut self, options: Vec<SelectOption>) -> Self {
79        self.options = options;
80        self
81    }
82
83    /// Set selected value
84    pub fn selected(mut self, value: impl Into<SharedString>) -> Self {
85        self.selected = Some(value.into());
86        self
87    }
88
89    /// Set placeholder
90    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
91        self.placeholder = Some(placeholder.into());
92        self
93    }
94
95    /// Set label
96    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
97        self.label = Some(label.into());
98        self
99    }
100
101    /// Set size
102    pub fn size(mut self, size: SelectSize) -> Self {
103        self.size = size;
104        self
105    }
106
107    /// Set disabled state
108    pub fn disabled(mut self, disabled: bool) -> Self {
109        self.disabled = disabled;
110        self
111    }
112
113    /// Set open state (for controlled component)
114    pub fn is_open(mut self, is_open: bool) -> Self {
115        self.is_open = is_open;
116        self
117    }
118
119    /// Set change handler
120    pub fn on_change(
121        mut self,
122        handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
123    ) -> Self {
124        self.on_change = Some(Box::new(handler));
125        self
126    }
127
128    /// Build into element
129    pub fn build(self) -> Div {
130        let (py, text_size_class) = match self.size {
131            SelectSize::Sm => (px(4.0), "sm"),
132            SelectSize::Md => (px(8.0), "md"),
133            SelectSize::Lg => (px(12.0), "lg"),
134        };
135
136        let mut container = div().flex().flex_col().gap_1();
137
138        // Label
139        if let Some(label) = self.label {
140            container = container.child(
141                div()
142                    .text_sm()
143                    .text_color(rgb(0xcccccc))
144                    .font_weight(FontWeight::MEDIUM)
145                    .child(label),
146            );
147        }
148
149        // Find selected option label
150        let selected_label = self.selected.as_ref().and_then(|val| {
151            self.options
152                .iter()
153                .find(|o| &o.value == val)
154                .map(|o| o.label.clone())
155        });
156
157        // Select trigger
158        let mut trigger = div()
159            .id(self.id)
160            .flex()
161            .items_center()
162            .justify_between()
163            .px_3()
164            .py(py)
165            .min_w(px(120.0))
166            .bg(rgb(0x1e1e1e))
167            .border_1()
168            .border_color(rgb(0x3a3a3a))
169            .rounded_md()
170            .cursor_pointer();
171
172        // Apply text size
173        trigger = match self.size {
174            SelectSize::Sm => trigger.text_xs(),
175            SelectSize::Md => trigger.text_sm(),
176            SelectSize::Lg => trigger,
177        };
178
179        if self.disabled {
180            trigger = trigger.opacity(0.5).cursor_not_allowed();
181        } else {
182            trigger = trigger.hover(|s| s.border_color(rgb(0x007acc)));
183        }
184
185        // Display value or placeholder
186        let display_text = if let Some(label) = selected_label {
187            div().text_color(rgb(0xffffff)).child(label)
188        } else if let Some(placeholder) = self.placeholder {
189            div().text_color(rgb(0x666666)).child(placeholder)
190        } else {
191            div().text_color(rgb(0x666666)).child("Select...")
192        };
193
194        trigger = trigger.child(display_text);
195
196        // Dropdown arrow
197        trigger = trigger.child(div().text_xs().text_color(rgb(0x666666)).child("▼"));
198
199        container = container.child(trigger);
200
201        // Dropdown menu (only shown when open)
202        if self.is_open {
203            let mut dropdown = div()
204                .absolute()
205                .top_full()
206                .left_0()
207                .right_0()
208                .mt_1()
209                .bg(rgb(0x2a2a2a))
210                .border_1()
211                .border_color(rgb(0x3a3a3a))
212                .rounded_md()
213                .shadow_lg()
214                .max_h(px(200.0))
215                .py_1();
216
217            for option in self.options {
218                let is_selected = self.selected.as_ref() == Some(&option.value);
219                let option_value = option.value.clone();
220
221                let mut option_el = div().px_3().py(px(6.0)).cursor_pointer();
222
223                // Apply text size
224                option_el = match self.size {
225                    SelectSize::Sm => option_el.text_xs(),
226                    SelectSize::Md => option_el.text_sm(),
227                    SelectSize::Lg => option_el,
228                };
229
230                if option.disabled {
231                    option_el = option_el.text_color(rgb(0x666666)).cursor_not_allowed();
232                } else if is_selected {
233                    option_el = option_el.bg(rgb(0x007acc)).text_color(rgb(0xffffff));
234                } else {
235                    option_el = option_el
236                        .text_color(rgb(0xcccccc))
237                        .hover(|s| s.bg(rgb(0x3a3a3a)));
238                }
239
240                option_el = option_el.child(option.label);
241                dropdown = dropdown.child(option_el);
242            }
243
244            container = container.relative().child(dropdown);
245        }
246
247        container
248    }
249}
250
251impl IntoElement for Select {
252    type Element = Div;
253
254    fn into_element(self) -> Self::Element {
255        self.build()
256    }
257}