Skip to main content

bexa_ui_core/widgets/
select.rs

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    // Colors
22    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    // State
30    open: bool,
31    hover: bool,
32    hover_index: Option<usize>,
33    focus: bool,
34    // Cached absolute position for overlay drawing (set in draw, used in handle_event)
35    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        // Cache absolute position for event handling
170        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        // Draw the select box
177        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        // Draw selected text
192        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        // Draw chevron icon
206        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        // Draw dropdown overlay when open
221        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            // Dropdown background
227            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            // Dropdown items
236            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                // Hover highlight
242                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                // Track hover over dropdown items
287                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                    // Check if clicking on dropdown item
302                    // We need cursor position — use cached abs position
303                    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                        // Clicked on the select box itself while open → close
310                        self.open = false;
311                        changed = true;
312                    } else {
313                        // Clicked outside → close
314                        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                    // Select hovered item or close
336                    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                    // Move selection down
360                    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}