1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone)]
10pub struct SelectTheme {
11 pub trigger_bg: Rgba,
13 pub trigger_border: Rgba,
15 pub trigger_border_hover: Rgba,
17 pub trigger_border_focused: Rgba,
19 pub dropdown_bg: Rgba,
21 pub dropdown_border: Rgba,
23 pub selected_bg: Rgba,
25 pub option_hover_bg: Rgba,
27 pub label_color: Rgba,
29 pub text_color: Rgba,
31 pub placeholder_color: Rgba,
33 pub option_text_color: Rgba,
35 pub selected_text_color: Rgba,
37 pub disabled_color: Rgba,
39 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
67pub enum SelectSize {
68 Sm,
70 #[default]
72 Md,
73 Lg,
75}
76
77#[derive(Clone)]
79pub struct SelectOption {
80 pub value: SharedString,
82 pub label: SharedString,
84 pub disabled: bool,
86}
87
88impl SelectOption {
89 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 pub fn disabled(mut self, disabled: bool) -> Self {
100 self.disabled = disabled;
101 self
102 }
103}
104
105pub 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 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 pub fn options(mut self, options: Vec<SelectOption>) -> Self {
138 self.options = options;
139 self
140 }
141
142 pub fn selected(mut self, value: impl Into<SharedString>) -> Self {
144 self.selected = Some(value.into());
145 self
146 }
147
148 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
150 self.placeholder = Some(placeholder.into());
151 self
152 }
153
154 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
156 self.label = Some(label.into());
157 self
158 }
159
160 pub fn size(mut self, size: SelectSize) -> Self {
162 self.size = size;
163 self
164 }
165
166 pub fn disabled(mut self, disabled: bool) -> Self {
168 self.disabled = disabled;
169 self
170 }
171
172 pub fn is_open(mut self, is_open: bool) -> Self {
174 self.is_open = is_open;
175 self
176 }
177
178 pub fn theme(mut self, theme: SelectTheme) -> Self {
180 self.theme = Some(theme);
181 self
182 }
183
184 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 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 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 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 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 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 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 trigger = trigger.child(div().text_xs().text_color(theme.arrow_color).child("▼"));
273
274 container = container.child(trigger);
275
276 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 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 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}