1use gpui::prelude::*;
7use gpui::{
8 deferred, div, px, Context, EventEmitter, FocusHandle, IntoElement, SharedString, Window,
9};
10
11use super::control_metrics;
12use crate::theme::{theme, Size};
13
14#[derive(Debug, Clone)]
16pub struct SelectEvent(pub usize);
17
18pub struct Select {
20 options: Vec<SharedString>,
21 selected: Option<usize>,
22 open: bool,
23 focus: FocusHandle,
24 placeholder: SharedString,
25 label: Option<SharedString>,
26 size: Size,
27 disabled: bool,
28}
29
30impl EventEmitter<SelectEvent> for Select {}
31
32impl Select {
33 pub fn new(cx: &mut Context<Self>) -> Self {
34 Select {
35 options: Vec::new(),
36 selected: None,
37 open: false,
38 focus: cx.focus_handle(),
39 placeholder: SharedString::new_static("Pick one"),
40 label: None,
41 size: Size::Sm,
42 disabled: false,
43 }
44 }
45
46 pub fn data<I, S>(mut self, options: I) -> Self
47 where
48 I: IntoIterator<Item = S>,
49 S: Into<SharedString>,
50 {
51 self.options = options.into_iter().map(Into::into).collect();
52 self
53 }
54
55 pub fn selected(mut self, index: usize) -> Self {
56 self.selected = Some(index);
57 self
58 }
59
60 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
61 self.placeholder = placeholder.into();
62 self
63 }
64
65 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
66 self.label = Some(label.into());
67 self
68 }
69
70 pub fn size(mut self, size: Size) -> Self {
71 self.size = size;
72 self
73 }
74
75 pub fn disabled(mut self, disabled: bool) -> Self {
76 self.disabled = disabled;
77 self
78 }
79
80 pub fn selected_index(&self) -> Option<usize> {
81 self.selected
82 }
83
84 pub fn selected_value(&self) -> Option<SharedString> {
85 self.selected.and_then(|i| self.options.get(i).cloned())
86 }
87}
88
89impl Render for Select {
90 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
91 let t = theme(cx);
92 let (height, pad_x, font) = control_metrics(self.size);
93 let radius = t.radius(t.default_radius);
94 let surface = t.surface().hsla();
95 let surface_hover = t.surface_hover().hsla();
96 let border = t.border().hsla();
97 let text_color = t.text().hsla();
98 let dimmed = t.dimmed().hsla();
99 let selected_bg = t.primary().alpha(0.12);
100 let font_sm = t.font_size(Size::Sm);
101
102 let selected = self.selected;
103 let chosen = selected.and_then(|i| self.options.get(i));
104 let has_value = chosen.is_some();
105 let value_text: SharedString =
106 chosen.cloned().unwrap_or_else(|| self.placeholder.clone());
107
108 let trigger = div()
109 .id("guise-select-trigger")
110 .track_focus(&self.focus)
111 .flex()
112 .items_center()
113 .justify_between()
114 .gap(px(8.0))
115 .h(px(height))
116 .px(px(pad_x))
117 .rounded(px(radius))
118 .border_1()
119 .border_color(border)
120 .bg(surface)
121 .text_size(px(font))
122 .text_color(if has_value { text_color } else { dimmed })
123 .child(value_text)
124 .child(
125 div()
126 .text_color(dimmed)
127 .child(SharedString::new_static("\u{25be}")),
128 )
129 .on_click(cx.listener(|this, _ev, _window, cx| {
130 if !this.disabled {
131 this.open = !this.open;
132 cx.notify();
133 }
134 }));
135
136 let mut wrap = div().relative().child(trigger);
137
138 if self.open && !self.disabled {
139 let mut menu = div()
140 .absolute()
141 .top(px(height + 6.0))
142 .left(px(0.0))
143 .right(px(0.0))
144 .flex()
145 .flex_col()
146 .gap(px(2.0))
147 .p(px(4.0))
148 .rounded(px(radius))
149 .border_1()
150 .border_color(border)
151 .bg(surface)
152 .shadow_md();
153
154 for (i, option) in self.options.iter().enumerate() {
155 let is_selected = Some(i) == selected;
156 let mut row = div()
157 .id(("guise-select-option", i))
158 .px(px(10.0))
159 .py(px(6.0))
160 .rounded(px(4.0))
161 .text_size(px(font))
162 .text_color(text_color)
163 .hover(move |s| s.bg(surface_hover))
164 .child(option.clone())
165 .on_click(cx.listener(move |this, _ev, _window, cx| {
166 this.selected = Some(i);
167 this.open = false;
168 cx.emit(SelectEvent(i));
169 cx.notify();
170 }));
171 if is_selected {
172 row = row.bg(selected_bg);
173 }
174 menu = menu.child(row);
175 }
176
177 wrap = wrap.child(deferred(menu));
178 }
179
180 let mut column = div().flex().flex_col().gap(px(4.0));
181 if let Some(label) = self.label.clone() {
182 column = column.child(
183 div()
184 .text_size(px(font_sm))
185 .text_color(text_color)
186 .child(label),
187 );
188 }
189 column = column.child(wrap);
190
191 if self.disabled {
192 column.opacity(0.6)
193 } else {
194 column
195 }
196 }
197}