1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum SelectSize {
11 Sm,
13 #[default]
15 Md,
16 Lg,
18}
19
20#[derive(Clone)]
22pub struct SelectOption {
23 pub value: SharedString,
25 pub label: SharedString,
27 pub disabled: bool,
29}
30
31impl SelectOption {
32 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 pub fn disabled(mut self, disabled: bool) -> Self {
43 self.disabled = disabled;
44 self
45 }
46}
47
48pub 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 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 pub fn options(mut self, options: Vec<SelectOption>) -> Self {
79 self.options = options;
80 self
81 }
82
83 pub fn selected(mut self, value: impl Into<SharedString>) -> Self {
85 self.selected = Some(value.into());
86 self
87 }
88
89 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
91 self.placeholder = Some(placeholder.into());
92 self
93 }
94
95 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
97 self.label = Some(label.into());
98 self
99 }
100
101 pub fn size(mut self, size: SelectSize) -> Self {
103 self.size = size;
104 self
105 }
106
107 pub fn disabled(mut self, disabled: bool) -> Self {
109 self.disabled = disabled;
110 self
111 }
112
113 pub fn is_open(mut self, is_open: bool) -> Self {
115 self.is_open = is_open;
116 self
117 }
118
119 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 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 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 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 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 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 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 trigger = trigger.child(div().text_xs().text_color(rgb(0x666666)).child("▼"));
198
199 container = container.child(trigger);
200
201 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 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}