1use std::cell::Cell;
2
3use glyphon::Metrics;
4use glyphon::cosmic_text::Align;
5use taffy::prelude::*;
6use winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent};
7use winit::keyboard::{Key, ModifiersState, NamedKey};
8
9use crate::framework::{DrawContext, EventContext, Widget};
10use crate::icons;
11use crate::signal::{Signal, SetSignal};
12
13pub struct Select {
14 options: Vec<String>,
15 selected: Signal<usize>,
16 set_selected: SetSignal<usize>,
17 metrics: Metrics,
18 padding: f32,
19 border_radius: f32,
20 item_height: Cell<f32>,
21 bg: [f32; 4],
23 border: [f32; 4],
24 text_color: [u8; 3],
25 dropdown_bg: [f32; 4],
26 dropdown_border: [f32; 4],
27 hover_bg: [f32; 4],
28 hover_text: [u8; 3],
29 open: bool,
31 hover: bool,
32 hover_index: Option<usize>,
33 focus: bool,
34 abs_x: Cell<f32>,
36 abs_y: Cell<f32>,
37 abs_w: Cell<f32>,
38 abs_h: Cell<f32>,
39}
40
41impl Select {
42 pub fn new(
43 options: Vec<String>,
44 selected: Signal<usize>,
45 set_selected: SetSignal<usize>,
46 metrics: Metrics,
47 ) -> Self {
48 Self {
49 options,
50 selected,
51 set_selected,
52 metrics,
53 padding: 8.0,
54 border_radius: 6.0,
55 item_height: Cell::new(0.0),
56 bg: [0.16, 0.28, 0.38, 1.0],
57 border: [0.4, 0.55, 0.7, 1.0],
58 text_color: [230, 230, 230],
59 dropdown_bg: [0.14, 0.24, 0.34, 1.0],
60 dropdown_border: [0.4, 0.55, 0.7, 1.0],
61 hover_bg: [0.20, 0.65, 0.85, 1.0],
62 hover_text: [255, 255, 255],
63 open: false,
64 hover: false,
65 hover_index: None,
66 focus: false,
67 abs_x: Cell::new(0.0),
68 abs_y: Cell::new(0.0),
69 abs_w: Cell::new(0.0),
70 abs_h: Cell::new(0.0),
71 }
72 }
73
74 pub fn with_padding(mut self, padding: f32) -> Self {
75 self.padding = padding;
76 self
77 }
78
79 pub fn with_border_radius(mut self, radius: f32) -> Self {
80 self.border_radius = radius;
81 self
82 }
83
84 pub fn with_colors(
85 mut self,
86 bg: [f32; 4],
87 border: [f32; 4],
88 text_color: [u8; 3],
89 ) -> Self {
90 self.bg = bg;
91 self.border = border;
92 self.text_color = text_color;
93 self
94 }
95
96 pub fn with_dropdown_colors(
97 mut self,
98 dropdown_bg: [f32; 4],
99 dropdown_border: [f32; 4],
100 hover_bg: [f32; 4],
101 hover_text: [u8; 3],
102 ) -> Self {
103 self.dropdown_bg = dropdown_bg;
104 self.dropdown_border = dropdown_border;
105 self.hover_bg = hover_bg;
106 self.hover_text = hover_text;
107 self
108 }
109
110 fn selected_text(&self) -> &str {
111 let idx = self.selected.get();
112 self.options.get(idx).map(|s| s.as_str()).unwrap_or("")
113 }
114
115 fn hit_test(&self, layout: &Layout, x: f32, y: f32) -> bool {
116 x >= layout.location.x
117 && x <= layout.location.x + layout.size.width
118 && y >= layout.location.y
119 && y <= layout.location.y + layout.size.height
120 }
121
122 fn dropdown_item_at(&self, x: f32, y: f32) -> Option<usize> {
123 if !self.open {
124 return None;
125 }
126 let item_h = self.item_height.get();
127 let dropdown_x = self.abs_x.get();
128 let dropdown_y = self.abs_y.get() + self.abs_h.get();
129 let dropdown_w = self.abs_w.get();
130
131 if x < dropdown_x || x > dropdown_x + dropdown_w {
132 return None;
133 }
134
135 let rel_y = y - dropdown_y;
136 if rel_y < 0.0 {
137 return None;
138 }
139
140 let idx = (rel_y / item_h) as usize;
141 if idx < self.options.len() {
142 Some(idx)
143 } else {
144 None
145 }
146 }
147}
148
149impl Widget for Select {
150 fn style(&self) -> Style {
151 let height = self.metrics.line_height + self.padding * 2.0;
152 Style {
153 size: Size {
154 width: Dimension::Percent(1.0),
155 height: Dimension::Length(height),
156 },
157 flex_shrink: 0.0,
158 ..Default::default()
159 }
160 }
161
162 fn draw(&self, ctx: &mut DrawContext) {
163 let layout = ctx.layout;
164 let x = layout.location.x;
165 let y = layout.location.y;
166 let w = layout.size.width;
167 let h = layout.size.height;
168
169 self.abs_x.set(x);
171 self.abs_y.set(y);
172 self.abs_w.set(w);
173 self.abs_h.set(h);
174 self.item_height.set(self.metrics.line_height + self.padding);
175
176 let border_w = if self.focus { 2.0 } else { 1.0 };
178 let border_c = if self.focus {
179 [0.3, 0.6, 0.9, 1.0]
180 } else {
181 self.border
182 };
183 ctx.renderer.fill_rect_styled(
184 (x, y, w, h),
185 self.bg,
186 self.border_radius,
187 border_w,
188 border_c,
189 );
190
191 let text_x = x + self.padding;
193 let text_y = y + (h - self.metrics.line_height) / 2.0;
194 let chevron_space = 24.0;
195 let text_w = (w - self.padding * 2.0 - chevron_space).max(0.0);
196 ctx.renderer.draw_text(
197 self.selected_text(),
198 (text_x, text_y),
199 self.text_color,
200 (text_w, self.metrics.line_height),
201 self.metrics,
202 Align::Left,
203 );
204
205 let icon_metrics = Metrics::new(self.metrics.font_size * 0.8, self.metrics.line_height);
207 let icon_x = x + w - self.padding - 16.0;
208 let icon_y = text_y;
209 let chevron = if self.open { icons::CHEVRON_UP } else { icons::CHEVRON_DOWN };
210 ctx.renderer.draw_text_with_font(
211 chevron,
212 (icon_x, icon_y),
213 self.text_color,
214 (16.0, self.metrics.line_height),
215 icon_metrics,
216 Align::Center,
217 icons::NERD_FONT_FAMILY,
218 );
219
220 if self.open {
222 let item_h = self.item_height.get();
223 let dropdown_h = item_h * self.options.len() as f32 + self.padding;
224 let dropdown_y = y + h;
225
226 ctx.renderer.overlay_fill_rect_styled(
228 (x, dropdown_y, w, dropdown_h),
229 self.dropdown_bg,
230 self.border_radius,
231 1.0,
232 self.dropdown_border,
233 );
234
235 for (i, option) in self.options.iter().enumerate() {
237 let iy = dropdown_y + self.padding * 0.5 + i as f32 * item_h;
238 let is_hover = self.hover_index == Some(i);
239 let is_selected = self.selected.get() == i;
240
241 if is_hover {
243 ctx.renderer.overlay_fill_rect_styled(
244 (x + 2.0, iy, w - 4.0, item_h),
245 self.hover_bg,
246 4.0,
247 0.0,
248 [0.0; 4],
249 );
250 }
251
252 let tc = if is_hover {
253 self.hover_text
254 } else if is_selected {
255 [180, 220, 255]
256 } else {
257 self.text_color
258 };
259
260 ctx.renderer.overlay_draw_text(
261 option,
262 (x + self.padding, iy + (item_h - self.metrics.line_height) / 2.0),
263 tc,
264 (w - self.padding * 2.0, self.metrics.line_height),
265 self.metrics,
266 Align::Left,
267 );
268 }
269 }
270 }
271
272 fn handle_event(&mut self, ctx: &mut EventContext) -> bool {
273 let layout = ctx.layout;
274 let mut changed = false;
275
276 match ctx.event {
277 WindowEvent::CursorMoved { position, .. } => {
278 let px = position.x as f32;
279 let py = position.y as f32;
280 let over = self.hit_test(layout, px, py);
281 if over != self.hover {
282 self.hover = over;
283 changed = true;
284 }
285
286 if self.open {
288 let new_hover = self.dropdown_item_at(px, py);
289 if new_hover != self.hover_index {
290 self.hover_index = new_hover;
291 changed = true;
292 }
293 }
294 }
295 WindowEvent::MouseInput {
296 state: ElementState::Pressed,
297 button: MouseButton::Left,
298 ..
299 } => {
300 if self.open {
301 if let Some(idx) = self.hover_index {
304 self.set_selected.set(idx);
305 self.open = false;
306 self.hover_index = None;
307 changed = true;
308 } else if self.hover {
309 self.open = false;
311 changed = true;
312 } else {
313 self.open = false;
315 changed = true;
316 }
317 } else if self.hover {
318 self.open = true;
319 changed = true;
320 }
321 }
322 _ => {}
323 }
324
325 changed
326 }
327
328 fn handle_key_event(&mut self, event: &KeyEvent, _modifiers: ModifiersState) -> bool {
329 if event.state != ElementState::Pressed {
330 return false;
331 }
332 match &event.logical_key {
333 Key::Named(NamedKey::Space) | Key::Named(NamedKey::Enter) => {
334 if self.open {
335 if let Some(idx) = self.hover_index {
337 self.set_selected.set(idx);
338 }
339 self.open = false;
340 } else {
341 self.open = true;
342 }
343 true
344 }
345 Key::Named(NamedKey::Escape) => {
346 if self.open {
347 self.open = false;
348 true
349 } else {
350 false
351 }
352 }
353 Key::Named(NamedKey::ArrowDown) => {
354 if self.open {
355 let current = self.hover_index.unwrap_or(self.selected.get());
356 let next = (current + 1).min(self.options.len().saturating_sub(1));
357 self.hover_index = Some(next);
358 } else {
359 let current = self.selected.get();
361 let next = (current + 1).min(self.options.len().saturating_sub(1));
362 self.set_selected.set(next);
363 }
364 true
365 }
366 Key::Named(NamedKey::ArrowUp) => {
367 if self.open {
368 let current = self.hover_index.unwrap_or(self.selected.get());
369 let next = current.saturating_sub(1);
370 self.hover_index = Some(next);
371 } else {
372 let current = self.selected.get();
373 let next = current.saturating_sub(1);
374 self.set_selected.set(next);
375 }
376 true
377 }
378 _ => false,
379 }
380 }
381
382 fn is_focusable(&self) -> bool {
383 true
384 }
385
386 fn set_focus(&mut self, focused: bool) {
387 self.focus = focused;
388 if !focused {
389 self.open = false;
390 }
391 }
392
393 fn activate(&mut self) {
394 self.open = !self.open;
395 }
396}