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