1use jag_draw::{Brush, Color, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 ElementState, EventHandler, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseClickEvent,
8 MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11
12use super::Element;
13
14#[derive(Clone)]
16pub struct Select {
17 pub rect: Rect,
19 pub label: String,
21 pub placeholder: String,
23 pub label_size: f32,
25 pub label_color: ColorLinPremul,
27 pub placeholder_color: ColorLinPremul,
29 pub is_placeholder: bool,
31 pub open: bool,
33 pub focused: bool,
35 pub options: Vec<String>,
37 pub selected_index: Option<usize>,
39 pub padding: [f32; 4],
41 pub bg_color: ColorLinPremul,
43 pub border_color: ColorLinPremul,
45 pub border_width: f32,
47 pub radius: f32,
49 pub validation_error: Option<String>,
51 pub focus_id: FocusId,
53}
54
55impl Select {
56 const OPTION_HEIGHT: f32 = 36.0;
58 const OVERLAY_PADDING: f32 = 4.0;
60
61 pub fn new(placeholder: impl Into<String>) -> Self {
63 let ph = placeholder.into();
64 Self {
65 rect: Rect {
66 x: 0.0,
67 y: 0.0,
68 w: 200.0,
69 h: 36.0,
70 },
71 label: ph.clone(),
72 placeholder: ph,
73 label_size: 14.0,
74 label_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
75 placeholder_color: ColorLinPremul::from_srgba_u8([160, 160, 160, 255]),
76 is_placeholder: true,
77 open: false,
78 focused: false,
79 options: Vec::new(),
80 selected_index: None,
81 padding: [8.0, 12.0, 8.0, 12.0],
82 bg_color: ColorLinPremul::from_srgba_u8([40, 40, 40, 255]),
83 border_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
84 border_width: 1.0,
85 radius: 6.0,
86 validation_error: None,
87 focus_id: FocusId(0),
88 }
89 }
90
91 pub fn toggle_open(&mut self) {
93 self.open = !self.open;
94 }
95
96 pub fn close(&mut self) {
98 self.open = false;
99 }
100
101 pub fn get_overlay_bounds(&self) -> Option<Rect> {
103 if !self.open || self.options.is_empty() {
104 return None;
105 }
106 let overlay_height =
107 (self.options.len() as f32 * Self::OPTION_HEIGHT) + (Self::OVERLAY_PADDING * 2.0);
108 Some(Rect {
109 x: self.rect.x,
110 y: self.rect.y + self.rect.h + 4.0,
111 w: self.rect.w,
112 h: overlay_height,
113 })
114 }
115
116 pub fn selected_option(&self) -> Option<&String> {
118 self.selected_index.and_then(|idx| self.options.get(idx))
119 }
120
121 pub fn set_selected_index(&mut self, index: Option<usize>) {
123 self.selected_index = index;
124 if let Some(idx) = index {
125 if idx < self.options.len() {
126 self.label = self.options[idx].clone();
127 self.is_placeholder = false;
128 }
129 } else {
130 self.label = self.placeholder.clone();
131 self.is_placeholder = true;
132 }
133 }
134
135 fn handle_field_click(&mut self, x: f32, y: f32) -> bool {
137 if x >= self.rect.x
138 && x <= self.rect.x + self.rect.w
139 && y >= self.rect.y
140 && y <= self.rect.y + self.rect.h
141 {
142 self.toggle_open();
143 true
144 } else {
145 false
146 }
147 }
148
149 fn handle_overlay_click(&mut self, x: f32, y: f32) -> bool {
151 if !self.open || self.options.is_empty() {
152 return false;
153 }
154 let overlay_bounds = match self.get_overlay_bounds() {
155 Some(b) => b,
156 None => return false,
157 };
158
159 if x < overlay_bounds.x
160 || x > overlay_bounds.x + overlay_bounds.w
161 || y < overlay_bounds.y
162 || y > overlay_bounds.y + overlay_bounds.h
163 {
164 return false;
165 }
166
167 let local_y = y - overlay_bounds.y - Self::OVERLAY_PADDING;
168 if local_y >= 0.0 {
169 let idx = (local_y / Self::OPTION_HEIGHT) as usize;
170 if idx < self.options.len() {
171 self.selected_index = Some(idx);
172 self.label = self.options[idx].clone();
173 self.is_placeholder = false;
174 self.open = false;
175 return true;
176 }
177 }
178 false
179 }
180
181 fn render_dropdown_overlay(&self, canvas: &mut Canvas, z: i32) {
183 let overlay_bounds = match self.get_overlay_bounds() {
184 Some(b) => b,
185 None => return,
186 };
187
188 let radius = 6.0;
189 let overlay_rrect = RoundedRect {
190 rect: overlay_bounds,
191 radii: RoundedRadii {
192 tl: radius,
193 tr: radius,
194 br: radius,
195 bl: radius,
196 },
197 };
198
199 let overlay_bg = Color::rgba(255, 255, 255, 255);
201 canvas.rounded_rect(overlay_rrect, Brush::Solid(overlay_bg), z);
202
203 jag_surface::shapes::draw_rounded_rectangle(
205 canvas,
206 overlay_rrect,
207 None,
208 Some(1.0),
209 Some(Brush::Solid(self.border_color)),
210 z + 1,
211 );
212
213 let pad_left = self.padding[3];
215 for (idx, option) in self.options.iter().enumerate() {
216 let option_y =
217 overlay_bounds.y + Self::OVERLAY_PADDING + (idx as f32 * Self::OPTION_HEIGHT);
218
219 let is_selected = self.selected_index == Some(idx);
220 if is_selected {
221 let highlight_bg = Color::rgba(220, 220, 224, 255);
222 canvas.fill_rect(
223 overlay_bounds.x,
224 option_y,
225 overlay_bounds.w,
226 Self::OPTION_HEIGHT,
227 Brush::Solid(highlight_bg),
228 z + 2,
229 );
230 }
231
232 let text_x = overlay_bounds.x + Self::OVERLAY_PADDING + pad_left;
233 let text_y = option_y + Self::OPTION_HEIGHT * 0.5 + self.label_size * 0.35;
234 let text_color = if is_selected {
235 Color::rgba(20, 24, 30, 255)
236 } else {
237 Color::rgba(34, 42, 52, 255)
238 };
239
240 canvas.draw_text_run_weighted(
241 [text_x, text_y],
242 option.clone(),
243 self.label_size,
244 400.0,
245 text_color,
246 z + 3,
247 );
248 }
249 }
250}
251
252impl Default for Select {
253 fn default() -> Self {
254 Self::new("Select...")
255 }
256}
257
258impl Element for Select {
263 fn rect(&self) -> Rect {
264 self.rect
265 }
266
267 fn set_rect(&mut self, rect: Rect) {
268 self.rect = rect;
269 }
270
271 fn render(&self, canvas: &mut Canvas, z: i32) {
272 let rrect = RoundedRect {
273 rect: self.rect,
274 radii: RoundedRadii {
275 tl: self.radius,
276 tr: self.radius,
277 br: self.radius,
278 bl: self.radius,
279 },
280 };
281
282 let has_error = self.validation_error.is_some();
283 let border_color = if has_error {
284 Color::rgba(220, 38, 38, 255)
285 } else if self.focused {
286 Color::rgba(63, 130, 246, 255)
287 } else {
288 self.border_color
289 };
290 let border_width = if has_error {
291 self.border_width.max(2.0)
292 } else if self.focused {
293 (self.border_width + 1.0).max(2.0)
294 } else {
295 self.border_width.max(1.0)
296 };
297
298 jag_surface::shapes::draw_snapped_rounded_rectangle(
299 canvas,
300 rrect,
301 Some(Brush::Solid(self.bg_color)),
302 Some(border_width),
303 Some(Brush::Solid(border_color)),
304 z,
305 );
306
307 let pad_top = self.padding[0];
309 let pad_left = self.padding[3];
310 let pad_bottom = self.padding[2];
311 let content_h = (self.rect.h - pad_top - pad_bottom).max(0.0);
312 let tp = [
313 self.rect.x + pad_left,
314 self.rect.y + pad_top + content_h * 0.5 + self.label_size * 0.35,
315 ];
316 let (text, color) = if self.is_placeholder {
317 (&self.placeholder, self.placeholder_color)
318 } else {
319 (&self.label, self.label_color)
320 };
321 canvas.draw_text_run_weighted(tp, text.clone(), self.label_size, 400.0, color, z + 2);
322
323 let pad_right = self.padding[1];
325 let chevron = if self.open { "\u{25B2}" } else { "\u{25BC}" };
326 let chevron_x = self.rect.x + self.rect.w - pad_right - 12.0;
327 let chevron_y = tp[1];
328 canvas.draw_text_run_weighted(
329 [chevron_x, chevron_y],
330 chevron.to_string(),
331 self.label_size * 0.7,
332 400.0,
333 self.label_color,
334 z + 3,
335 );
336
337 if self.open && !self.options.is_empty() {
339 self.render_dropdown_overlay(canvas, z + 1000);
340 }
341
342 if let Some(ref error_msg) = self.validation_error {
344 let error_size = (self.label_size * 0.9).max(12.0);
345 let baseline_offset = error_size * 0.8;
346 let top_gap = 3.0;
347 let error_y = self.rect.y + self.rect.h + top_gap + baseline_offset;
348 let error_color = ColorLinPremul::from_srgba_u8([220, 38, 38, 255]);
349
350 canvas.draw_text_run_weighted(
351 [self.rect.x + pad_left, error_y],
352 error_msg.clone(),
353 error_size,
354 400.0,
355 error_color,
356 z + 5,
357 );
358 }
359 }
360
361 fn focus_id(&self) -> Option<FocusId> {
362 Some(self.focus_id)
363 }
364}
365
366impl EventHandler for Select {
371 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
372 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
373 return EventResult::Ignored;
374 }
375 if self.open && self.handle_overlay_click(event.x, event.y) {
376 return EventResult::Handled;
377 }
378 if self.handle_field_click(event.x, event.y) {
379 return EventResult::Handled;
380 }
381 EventResult::Ignored
382 }
383
384 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
385 if event.state != ElementState::Pressed {
386 return EventResult::Ignored;
387 }
388 if !self.focused && !self.open {
389 return EventResult::Ignored;
390 }
391 match event.key {
392 KeyCode::ArrowDown => {
393 if !self.open {
394 self.open = true;
395 } else if !self.options.is_empty() {
396 let new_idx = match self.selected_index {
397 Some(idx) if idx + 1 < self.options.len() => idx + 1,
398 Some(idx) => idx,
399 None => 0,
400 };
401 self.set_selected_index(Some(new_idx));
402 }
403 EventResult::Handled
404 }
405 KeyCode::ArrowUp => {
406 if self.open && !self.options.is_empty() {
407 let new_idx = match self.selected_index {
408 Some(idx) if idx > 0 => idx - 1,
409 Some(idx) => idx,
410 None => 0,
411 };
412 self.set_selected_index(Some(new_idx));
413 EventResult::Handled
414 } else {
415 EventResult::Ignored
416 }
417 }
418 KeyCode::Enter => {
419 self.open = !self.open;
420 EventResult::Handled
421 }
422 KeyCode::Escape => {
423 if self.open {
424 self.open = false;
425 EventResult::Handled
426 } else {
427 EventResult::Ignored
428 }
429 }
430 KeyCode::Space => {
431 self.toggle_open();
432 EventResult::Handled
433 }
434 _ => EventResult::Ignored,
435 }
436 }
437
438 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
439 EventResult::Ignored
440 }
441
442 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
443 EventResult::Ignored
444 }
445
446 fn is_focused(&self) -> bool {
447 self.focused
448 }
449
450 fn set_focused(&mut self, focused: bool) {
451 self.focused = focused;
452 }
453
454 fn contains_point(&self, x: f32, y: f32) -> bool {
455 if x >= self.rect.x
457 && x <= self.rect.x + self.rect.w
458 && y >= self.rect.y
459 && y <= self.rect.y + self.rect.h
460 {
461 return true;
462 }
463 if let Some(ob) = self.get_overlay_bounds()
465 && x >= ob.x
466 && x <= ob.x + ob.w
467 && y >= ob.y
468 && y <= ob.y + ob.h
469 {
470 return true;
471 }
472 false
473 }
474}
475
476#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn select_new_defaults() {
486 let sel = Select::new("Choose...");
487 assert_eq!(sel.placeholder, "Choose...");
488 assert!(sel.is_placeholder);
489 assert!(!sel.open);
490 assert!(sel.selected_index.is_none());
491 }
492
493 #[test]
494 fn select_set_selected_index() {
495 let mut sel = Select::new("Pick");
496 sel.options = vec!["A".into(), "B".into(), "C".into()];
497 sel.set_selected_index(Some(1));
498 assert_eq!(sel.selected_index, Some(1));
499 assert_eq!(sel.label, "B");
500 assert!(!sel.is_placeholder);
501
502 sel.set_selected_index(None);
503 assert_eq!(sel.label, "Pick");
504 assert!(sel.is_placeholder);
505 }
506
507 #[test]
508 fn select_toggle_open() {
509 let mut sel = Select::new("Pick");
510 assert!(!sel.open);
511 sel.toggle_open();
512 assert!(sel.open);
513 sel.toggle_open();
514 assert!(!sel.open);
515 }
516
517 #[test]
518 fn select_contains_point_field() {
519 let mut sel = Select::new("Pick");
520 sel.rect = Rect {
521 x: 10.0,
522 y: 10.0,
523 w: 200.0,
524 h: 36.0,
525 };
526 assert!(sel.contains_point(100.0, 25.0));
527 assert!(!sel.contains_point(0.0, 0.0));
528 }
529
530 #[test]
531 fn select_keyboard_open_close() {
532 let mut sel = Select::new("Pick");
533 sel.focused = true;
534 sel.options = vec!["X".into()];
535
536 let down = KeyboardEvent {
537 key: KeyCode::ArrowDown,
538 state: ElementState::Pressed,
539 modifiers: Default::default(),
540 text: None,
541 };
542 assert_eq!(sel.handle_keyboard(&down), EventResult::Handled);
543 assert!(sel.open);
544
545 let esc = KeyboardEvent {
546 key: KeyCode::Escape,
547 state: ElementState::Pressed,
548 modifiers: Default::default(),
549 text: None,
550 };
551 assert_eq!(sel.handle_keyboard(&esc), EventResult::Handled);
552 assert!(!sel.open);
553 }
554}