1use alloc::{string::String, vec::Vec};
23use rlvgl_core::draw::draw_widget_bg;
24use rlvgl_core::event::Event;
25use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr};
26use rlvgl_core::renderer::Renderer;
27use rlvgl_core::style::Style;
28use rlvgl_core::widget::{Color, Rect, Widget};
29
30const ROW_HEIGHT: i32 = 16;
32const TRIGGER_HEIGHT: i32 = 20;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DropdownDir {
38 Down,
40 Up,
42}
43
44pub struct Dropdown {
49 bounds: Rect,
50 options: Vec<String>,
51 selected: usize,
53 open: bool,
55 dir: DropdownDir,
57 symbol: Option<String>,
59 selected_highlight: bool,
61 pub style: Style,
63 pub item_style: Style,
65 pub selected_color: Color,
67 pub text_color: Color,
69 font: WidgetFont,
72}
73
74impl Dropdown {
75 pub fn new(bounds: Rect) -> Self {
78 Self {
79 bounds,
80 options: Vec::new(),
81 selected: 0,
82 open: false,
83 dir: DropdownDir::Down,
84 symbol: None,
85 selected_highlight: true,
86 style: Style::default(),
87 item_style: Style {
88 bg_color: Color(240, 240, 240, 255),
89 ..Style::default()
90 },
91 selected_color: Color(160, 200, 240, 255),
92 text_color: Color(30, 30, 30, 255),
93 font: WidgetFont::new(),
94 }
95 }
96
97 pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
100 self.font.set(font);
101 }
102
103 pub fn set_options(&mut self, options: &[impl AsRef<str>]) {
109 self.options = options.iter().map(|s| s.as_ref().into()).collect();
110 self.selected = 0;
111 }
112
113 pub fn options(&self) -> &[String] {
115 &self.options
116 }
117
118 pub fn option_count(&self) -> usize {
120 self.options.len()
121 }
122
123 pub fn set_selected(&mut self, index: usize) {
127 self.selected = index.min(self.options.len().saturating_sub(1));
128 }
129
130 pub fn selected(&self) -> usize {
132 self.selected
133 }
134
135 pub fn selected_text(&self) -> &str {
137 self.options
138 .get(self.selected)
139 .map(|s| s.as_str())
140 .unwrap_or("")
141 }
142
143 pub fn open(&mut self) {
147 self.open = true;
148 }
149
150 pub fn close(&mut self) {
152 self.open = false;
153 }
154
155 pub fn toggle(&mut self) {
157 self.open = !self.open;
158 }
159
160 pub fn is_open(&self) -> bool {
162 self.open
163 }
164
165 pub fn set_dir(&mut self, dir: DropdownDir) {
169 self.dir = dir;
170 }
171
172 pub fn dir(&self) -> DropdownDir {
174 self.dir
175 }
176
177 pub fn set_symbol(&mut self, sym: Option<&str>) {
179 self.symbol = sym.map(|s| s.into());
180 }
181
182 pub fn symbol(&self) -> Option<&str> {
184 self.symbol.as_deref()
185 }
186
187 pub fn set_selected_highlight(&mut self, enable: bool) {
191 self.selected_highlight = enable;
192 }
193
194 pub fn selected_highlight(&self) -> bool {
196 self.selected_highlight
197 }
198
199 pub fn navigate_next(&mut self) {
205 if self.options.is_empty() {
206 return;
207 }
208 self.selected = (self.selected + 1) % self.options.len();
209 }
210
211 pub fn navigate_prev(&mut self) {
215 if self.options.is_empty() {
216 return;
217 }
218 let n = self.options.len();
219 self.selected = (self.selected + n - 1) % n;
220 }
221
222 pub fn activate_selected(&mut self) {
226 self.open = false;
227 }
228
229 pub fn close_key(&mut self) {
233 self.open = false;
234 }
235
236 fn trigger_rect(&self) -> Rect {
240 match self.dir {
241 DropdownDir::Down => Rect {
242 x: self.bounds.x,
243 y: self.bounds.y,
244 width: self.bounds.width,
245 height: TRIGGER_HEIGHT.min(self.bounds.height),
246 },
247 DropdownDir::Up => {
248 let h = TRIGGER_HEIGHT.min(self.bounds.height);
249 Rect {
250 x: self.bounds.x,
251 y: self.bounds.y + self.bounds.height - h,
252 width: self.bounds.width,
253 height: h,
254 }
255 }
256 }
257 }
258
259 fn list_rect(&self) -> Rect {
261 let tr = self.trigger_rect();
262 match self.dir {
263 DropdownDir::Down => Rect {
264 x: self.bounds.x,
265 y: tr.y + tr.height,
266 width: self.bounds.width,
267 height: (self.bounds.height - tr.height).max(0),
268 },
269 DropdownDir::Up => Rect {
270 x: self.bounds.x,
271 y: self.bounds.y,
272 width: self.bounds.width,
273 height: (self.bounds.height - tr.height).max(0),
274 },
275 }
276 }
277}
278
279impl Widget for Dropdown {
280 fn bounds(&self) -> Rect {
281 self.bounds
282 }
283
284 fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
285 Some(&mut self.font)
286 }
287
288 fn set_bounds(&mut self, bounds: Rect) {
289 self.bounds = bounds;
290 }
291
292 fn draw(&self, renderer: &mut dyn Renderer) {
293 let a = self.style.alpha;
294 let font = self.font.resolve();
295
296 let tr = self.trigger_rect();
298 draw_widget_bg(renderer, tr, &self.style);
299
300 let text = self.selected_text();
302 let baseline = tr.y + 14; let text_pos = (tr.x + 4, baseline);
304 let shaped = shape_text_ltr(font, text, text_pos, 0);
305 renderer.draw_text_shaped(&shaped, (0, 0), self.text_color.with_alpha(a));
306
307 if let Some(sym) = self.symbol.as_deref() {
309 let sym_x = tr.x + tr.width - 14;
310 let shaped_sym = shape_text_ltr(font, sym, (sym_x, baseline), 0);
311 renderer.draw_text_shaped(&shaped_sym, (0, 0), self.text_color.with_alpha(a));
312 } else {
313 let tip_x = tr.x + tr.width - 10;
316 let tip_y = tr.y + 8;
317 let tri_w = 6i32;
318 for row in 0..4i32 {
319 let w = tri_w - row * 2;
320 if w <= 0 {
321 break;
322 }
323 renderer.fill_rect(
324 Rect {
325 x: tip_x + row,
326 y: tip_y + row,
327 width: w,
328 height: 1,
329 },
330 self.text_color.with_alpha(a),
331 );
332 }
333 }
334
335 if !self.open {
337 return;
338 }
339 let lr = self.list_rect();
340 if lr.height <= 0 {
341 return;
342 }
343
344 draw_widget_bg(renderer, lr, &self.item_style);
345
346 let item_a = self.item_style.alpha;
347 for (i, option) in self.options.iter().enumerate() {
348 let row_y = lr.y + i as i32 * ROW_HEIGHT;
349 if row_y + ROW_HEIGHT < lr.y {
350 continue;
351 }
352 if row_y >= lr.y + lr.height {
353 break;
354 }
355
356 let row_rect = Rect {
357 x: lr.x,
358 y: row_y,
359 width: lr.width,
360 height: ROW_HEIGHT.min(lr.y + lr.height - row_y),
361 };
362
363 if self.selected_highlight && self.selected == i {
365 renderer.fill_rect(row_rect, self.selected_color.with_alpha(item_a));
366 }
367
368 let baseline = row_y + ROW_HEIGHT - 2;
370 let opt_pos = (lr.x + 4, baseline);
371 let shaped_opt = shape_text_ltr(font, option, opt_pos, 0);
372 renderer.draw_text_shaped(&shaped_opt, (0, 0), self.text_color.with_alpha(item_a));
373 }
374 }
375
376 fn handle_event(&mut self, event: &Event) -> bool {
377 match event {
378 Event::PressRelease { x, y } => {
379 let tr = self.trigger_rect();
380 if *x >= tr.x && *x < tr.x + tr.width && *y >= tr.y && *y < tr.y + tr.height {
382 self.open = !self.open;
383 return true;
384 }
385
386 if self.open {
388 let lr = self.list_rect();
389 if *x >= lr.x && *x < lr.x + lr.width && *y >= lr.y && *y < lr.y + lr.height {
390 let idx = ((*y - lr.y) / ROW_HEIGHT) as usize;
391 if idx < self.options.len() {
392 self.selected = idx;
393 self.open = false;
394 return true;
395 }
396 }
397 }
398 false
399 }
400 _ => false,
401 }
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use alloc::string::ToString;
409
410 fn rect(x: i32, y: i32, w: i32, h: i32) -> Rect {
411 Rect {
412 x,
413 y,
414 width: w,
415 height: h,
416 }
417 }
418
419 struct NullRenderer;
420 impl rlvgl_core::renderer::Renderer for NullRenderer {
421 fn fill_rect(&mut self, _: Rect, _: Color) {}
422 fn draw_text(&mut self, _: (i32, i32), _: &str, _: Color) {}
423 }
424
425 #[test]
426 fn new_starts_closed() {
427 let d = Dropdown::new(rect(0, 0, 120, 120));
428 assert!(!d.is_open());
429 assert_eq!(d.selected(), 0);
430 }
431
432 #[test]
433 fn set_options_resets_selection() {
434 let mut d = Dropdown::new(rect(0, 0, 120, 120));
435 d.set_options(&["A", "B", "C"]);
436 d.set_selected(2);
437 assert_eq!(d.selected(), 2);
438 d.set_options(&["X", "Y"]);
439 assert_eq!(d.selected(), 0);
440 }
441
442 #[test]
443 fn options_are_owned() {
444 let mut d = Dropdown::new(rect(0, 0, 120, 120));
445 {
446 let opts = alloc::vec!["Alpha".to_string(), "Beta".to_string()];
447 d.set_options(&opts);
448 }
449 assert_eq!(d.option_count(), 2);
451 assert_eq!(d.options()[0], "Alpha");
452 }
453
454 #[test]
455 fn open_close_toggle() {
456 let mut d = Dropdown::new(rect(0, 0, 120, 120));
457 d.open();
458 assert!(d.is_open());
459 d.close();
460 assert!(!d.is_open());
461 d.toggle();
462 assert!(d.is_open());
463 d.toggle();
464 assert!(!d.is_open());
465 }
466
467 #[test]
468 fn navigate_next_wraps() {
469 let mut d = Dropdown::new(rect(0, 0, 120, 120));
470 d.set_options(&["A", "B", "C"]);
471 d.navigate_next();
472 assert_eq!(d.selected(), 1);
473 d.navigate_next();
474 assert_eq!(d.selected(), 2);
475 d.navigate_next(); assert_eq!(d.selected(), 0);
477 }
478
479 #[test]
480 fn navigate_prev_wraps() {
481 let mut d = Dropdown::new(rect(0, 0, 120, 120));
482 d.set_options(&["A", "B", "C"]);
483 d.navigate_prev(); assert_eq!(d.selected(), 2);
485 }
486
487 #[test]
488 fn activate_selected_closes() {
489 let mut d = Dropdown::new(rect(0, 0, 120, 120));
490 d.set_options(&["A", "B"]);
491 d.open();
492 d.navigate_next();
493 d.activate_selected();
494 assert!(!d.is_open());
495 assert_eq!(d.selected(), 1);
496 }
497
498 #[test]
499 fn close_key_closes_without_changing_selection() {
500 let mut d = Dropdown::new(rect(0, 0, 120, 120));
501 d.set_options(&["A", "B", "C"]);
502 d.set_selected(1);
503 d.open();
504 d.close_key();
505 assert!(!d.is_open());
506 assert_eq!(d.selected(), 1);
507 }
508
509 #[test]
510 fn press_release_on_trigger_toggles_open() {
511 let mut d = Dropdown::new(rect(0, 0, 120, 120));
512 d.set_options(&["A"]);
513 assert!(d.handle_event(&Event::PressRelease { x: 5, y: 5 }));
515 assert!(d.is_open());
516 assert!(d.handle_event(&Event::PressRelease { x: 5, y: 5 }));
517 assert!(!d.is_open());
518 }
519
520 #[test]
521 fn press_release_on_option_commits_selection() {
522 let mut d = Dropdown::new(rect(0, 0, 120, 120));
523 d.set_options(&["A", "B", "C"]);
524 d.open();
525 let list_y = 20 + ROW_HEIGHT;
527 assert!(d.handle_event(&Event::PressRelease {
528 x: 5,
529 y: list_y + 2
530 }));
531 assert!(!d.is_open());
532 assert_eq!(d.selected(), 1);
533 }
534
535 #[test]
536 fn set_selected_clamps() {
537 let mut d = Dropdown::new(rect(0, 0, 120, 120));
538 d.set_options(&["A", "B"]);
539 d.set_selected(999);
540 assert_eq!(d.selected(), 1);
541 }
542
543 #[test]
544 fn selected_text_returns_correct_option() {
545 let mut d = Dropdown::new(rect(0, 0, 120, 120));
546 d.set_options(&["Alpha", "Beta"]);
547 d.set_selected(1);
548 assert_eq!(d.selected_text(), "Beta");
549 }
550
551 #[test]
552 fn set_bounds_adopted() {
553 let mut d = Dropdown::new(rect(0, 0, 120, 120));
554 d.set_bounds(rect(10, 20, 200, 150));
555 assert_eq!(d.bounds(), rect(10, 20, 200, 150));
556 }
557
558 #[test]
559 fn draw_does_not_panic() {
560 let mut d = Dropdown::new(rect(0, 0, 120, 120));
561 d.set_options(&["Option 1", "Option 2", "Option 3"]);
562 d.open();
563 let mut r = NullRenderer;
564 d.draw(&mut r);
565 }
566
567 #[test]
568 fn navigate_empty_options_no_panic() {
569 let mut d = Dropdown::new(rect(0, 0, 120, 120));
570 d.navigate_next();
571 d.navigate_prev();
572 assert_eq!(d.selected(), 0);
574 }
575}