1use super::{Widget, element::Element};
35use alloc::{boxed::Box, string::String, vec::Vec};
36use core::marker::PhantomData;
37use embedded_graphics::{
38 pixelcolor::PixelColor, prelude::*, primitives::Rectangle, text::Alignment,
39};
40use zest_core::{
41 Constraints, Horizontal, Length, RenderError, Renderer, TouchPhase, UiAction, Vertical,
42 WidgetId,
43};
44use zest_theme::Theme;
45
46const ROW_HEIGHT: u32 = 36;
48const TEXT_PAD: i32 = 8;
50
51pub struct Dropdown<'a, C: PixelColor, M: Clone> {
56 options: Vec<String>,
57 selected: usize,
58 is_open: bool,
59 placeholder: String,
60 id: Option<WidgetId>,
61 on_toggle: Option<Box<dyn Fn(bool) -> M + 'a>>,
62 on_select: Option<Box<dyn Fn(usize) -> M + 'a>>,
63 width: Length,
64 height: Length,
65 stack: Option<Element<'a, C, M>>,
67 option_count: usize,
70}
71
72impl<'a, C: PixelColor + 'a, M: Clone + 'a> Dropdown<'a, C, M> {
73 pub fn new() -> Self {
77 Self {
78 options: Vec::new(),
79 selected: 0,
80 is_open: false,
81 placeholder: String::new(),
82 id: None,
83 on_toggle: None,
84 on_select: None,
85 width: Length::Fill,
86 height: Length::Fixed(ROW_HEIGHT),
87 stack: None,
88 option_count: 0,
89 }
90 }
91
92 #[must_use]
94 pub fn width(mut self, width: impl Into<Length>) -> Self {
95 self.width = width.into();
96 self
97 }
98
99 #[must_use]
102 pub fn height(mut self, height: impl Into<Length>) -> Self {
103 self.height = height.into();
104 self
105 }
106
107 #[must_use]
110 pub fn options(mut self, options: &[&str]) -> Self {
111 self.options = options.iter().map(|s| String::from(*s)).collect();
112 self.option_count = self.options.len();
113 self
114 }
115
116 #[must_use]
118 pub fn selected(mut self, index: usize) -> Self {
119 self.selected = index;
120 self
121 }
122
123 #[must_use]
125 pub fn open(mut self, open: bool) -> Self {
126 self.is_open = open;
127 self
128 }
129
130 #[must_use]
133 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
134 self.placeholder = placeholder.into();
135 self
136 }
137
138 #[must_use]
141 pub fn id(mut self, id: WidgetId) -> Self {
142 self.id = Some(id);
143 self
144 }
145
146 #[must_use]
149 pub fn on_toggle<F: Fn(bool) -> M + 'a>(mut self, f: F) -> Self {
150 self.on_toggle = Some(Box::new(f));
151 self
152 }
153
154 #[must_use]
157 pub fn on_select<F: Fn(usize) -> M + 'a>(mut self, f: F) -> Self {
158 self.on_select = Some(Box::new(f));
159 self
160 }
161
162 fn build(&mut self) -> Element<'a, C, M> {
167 let label = self
168 .options
169 .get(self.selected)
170 .cloned()
171 .unwrap_or_else(|| self.placeholder.clone());
172
173 let field = DropdownField {
174 rect: Rectangle::zero(),
175 id: self.id,
176 label,
177 open: self.is_open,
178 on_toggle: self.on_toggle.take(),
179 focused: false,
180 pressed: false,
181 width: self.width,
182 height: self.height,
183 _color: PhantomData,
184 };
185
186 let mut stack = super::stack::Stack::new()
187 .width(self.width)
188 .height(self.height);
189 stack = stack.push_aligned(field, Horizontal::Left, Vertical::Top);
190
191 if self.is_open && !self.options.is_empty() {
192 let list = DropdownList {
193 rect: Rectangle::zero(),
194 base_id: self.id,
195 options: self.options.clone(),
196 selected: self.selected,
197 on_select: self.on_select.take(),
198 focused: None,
199 pressed: None,
200 _color: PhantomData,
201 };
202 stack = stack.push_aligned(list, Horizontal::Left, Vertical::Top);
205 }
206
207 Element::new(stack)
208 }
209
210 fn ensure_built(&mut self) {
212 if self.stack.is_none() {
213 self.stack = Some(self.build());
214 }
215 }
216}
217
218impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for Dropdown<'a, C, M> {
219 fn default() -> Self {
220 Self::new()
221 }
222}
223
224impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for Dropdown<'a, C, M> {
225 fn measure(&mut self, constraints: Constraints) -> Size {
226 self.ensure_built();
227 let w = self
230 .width
231 .resolve(constraints.max.width, constraints.max.width);
232 let h = self.height.resolve(ROW_HEIGHT, constraints.max.height);
233 constraints.clamp(Size::new(w, h))
234 }
235
236 fn preferred_size(&self) -> (Length, Length) {
237 (self.width, self.height)
238 }
239
240 fn arrange(&mut self, rect: Rectangle) {
241 self.ensure_built();
242 let extra = if self.is_open {
246 ROW_HEIGHT.saturating_mul(self.option_count as u32)
247 } else {
248 0
249 };
250 let region = Rectangle::new(
251 rect.top_left,
252 Size::new(rect.size.width, rect.size.height + extra),
253 );
254 if let Some(stack) = self.stack.as_mut() {
255 stack.arrange(region);
256 }
257 }
258
259 fn rect(&self) -> Rectangle {
260 self.stack.as_ref().map_or(Rectangle::zero(), |s| s.rect())
261 }
262
263 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
264 self.ensure_built();
265 self.stack
266 .as_mut()
267 .and_then(|s| s.handle_touch(point, phase))
268 }
269
270 fn mark_pressed(&mut self, point: Point) {
271 self.ensure_built();
272 if let Some(stack) = self.stack.as_mut() {
273 stack.mark_pressed(point);
274 }
275 }
276
277 fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
278 if let Some(id) = self.id
279 && self.on_toggle.is_some()
280 {
281 out.push(id);
282 }
283
284 if self.is_open && self.on_select.is_some() {
285 for index in 0..self.options.len() {
286 if let Some(id) = self.row_id(index) {
287 out.push(id);
288 }
289 }
290 }
291 }
292
293 fn sync_focus(&mut self, focused: Option<WidgetId>) {
294 self.ensure_built();
295 if let Some(stack) = self.stack.as_mut() {
296 stack.sync_focus(focused);
297 }
298 }
299
300 fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
301 self.ensure_built();
302 self.stack
303 .as_mut()
304 .and_then(|stack| stack.route_action(target, action))
305 }
306
307 fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
308 self.stack
309 .as_ref()
310 .and_then(|stack| stack.focus_rect(target))
311 }
312
313 fn focus_at(&self, point: Point) -> Option<WidgetId> {
314 self.stack.as_ref().and_then(|stack| stack.focus_at(point))
315 }
316
317 fn draw<'t>(
318 &self,
319 renderer: &mut dyn Renderer<C>,
320 theme: &Theme<'t, C>,
321 ) -> Result<(), RenderError> {
322 if let Some(stack) = &self.stack {
323 stack.draw(renderer, theme)?;
324 }
325 Ok(())
326 }
327}
328
329impl<'a, C: PixelColor + 'a, M: Clone + 'a> Dropdown<'a, C, M> {
330 fn row_id(&self, index: usize) -> Option<WidgetId> {
331 self.id
332 .map(|id| WidgetId::new(id.raw().wrapping_add(index as u64 + 1)))
333 }
334}
335
336struct DropdownField<'a, C: PixelColor, M: Clone> {
341 rect: Rectangle,
342 id: Option<WidgetId>,
343 label: String,
344 open: bool,
345 on_toggle: Option<Box<dyn Fn(bool) -> M + 'a>>,
346 focused: bool,
347 pressed: bool,
348 width: Length,
349 height: Length,
350 _color: PhantomData<C>,
351}
352
353impl<C: PixelColor, M: Clone> DropdownField<'_, C, M> {
354 fn hit_test(&self, point: Point) -> bool {
355 let tl = self.rect.top_left;
356 let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
357 point.x >= tl.x && point.x < br.x && point.y >= tl.y && point.y < br.y
358 }
359}
360
361impl<C: PixelColor, M: Clone> Widget<C, M> for DropdownField<'_, C, M> {
362 fn measure(&mut self, constraints: Constraints) -> Size {
363 let w = self
364 .width
365 .resolve(constraints.max.width, constraints.max.width);
366 let h = self.height.resolve(ROW_HEIGHT, constraints.max.height);
367 constraints.clamp(Size::new(w, h))
368 }
369
370 fn preferred_size(&self) -> (Length, Length) {
371 (self.width, self.height)
372 }
373
374 fn arrange(&mut self, rect: Rectangle) {
375 self.rect = rect;
376 }
377
378 fn rect(&self) -> Rectangle {
379 self.rect
380 }
381
382 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
383 if self.on_toggle.is_none() || !self.hit_test(point) {
384 if matches!(phase, TouchPhase::Up | TouchPhase::Moved) {
385 self.pressed = false;
386 }
387 return None;
388 }
389 match phase {
390 TouchPhase::Down => {
391 self.pressed = true;
392 None
393 }
394 TouchPhase::Up => {
395 if self.pressed {
396 self.pressed = false;
397 let open = self.open;
398 self.on_toggle.as_ref().map(|cb| cb(!open))
399 } else {
400 None
401 }
402 }
403 TouchPhase::Moved => None,
404 }
405 }
406
407 fn mark_pressed(&mut self, point: Point) {
408 if self.on_toggle.is_some() && self.hit_test(point) {
409 self.pressed = true;
410 }
411 }
412
413 fn widget_id(&self) -> Option<WidgetId> {
414 self.id
415 }
416
417 fn is_focusable(&self) -> bool {
418 self.id.is_some() && self.on_toggle.is_some()
419 }
420
421 fn handle_action(&mut self, action: UiAction) -> Option<M> {
422 match action {
423 UiAction::Activate => self.on_toggle.as_ref().map(|cb| cb(!self.open)),
424 _ => None,
425 }
426 }
427
428 fn sync_focus(&mut self, focused: Option<WidgetId>) {
429 self.focused = self.id.is_some() && self.id == focused;
430 }
431
432 fn focus_at(&self, point: Point) -> Option<WidgetId> {
433 if self.is_focusable() && self.hit_test(point) {
434 self.id
435 } else {
436 None
437 }
438 }
439
440 fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
441 if self.id == Some(target) {
442 Some(self.rect)
443 } else {
444 None
445 }
446 }
447
448 fn draw<'t>(
449 &self,
450 renderer: &mut dyn Renderer<C>,
451 theme: &Theme<'t, C>,
452 ) -> Result<(), RenderError> {
453 let comp = &theme.button;
454 let bg = if self.pressed {
455 comp.pressed
456 } else {
457 comp.base
458 };
459 let border = if self.focused {
460 theme.accent.base
461 } else {
462 comp.border
463 };
464 renderer.fill_rect(self.rect, bg)?;
465 renderer.stroke_rect(self.rect, border)?;
466
467 let font = theme.default_font();
468 let text_y = self.rect.top_left.y
469 + self.rect.size.height as i32 / 2
470 + font.character_size.height as i32 / 3;
471 renderer.draw_text(
472 &self.label,
473 Point::new(self.rect.top_left.x + TEXT_PAD, text_y),
474 font,
475 comp.on_base,
476 Alignment::Left,
477 )?;
478
479 let cx = self.rect.top_left.x + self.rect.size.width as i32 - TEXT_PAD - 6;
482 let cy = self.rect.top_left.y + self.rect.size.height as i32 / 2;
483 if self.open {
484 renderer.stroke_line(
485 Point::new(cx, cy + 3),
486 Point::new(cx + 6, cy + 3),
487 comp.on_base,
488 2,
489 )?;
490 renderer.stroke_line(
491 Point::new(cx, cy + 3),
492 Point::new(cx + 3, cy - 3),
493 comp.on_base,
494 2,
495 )?;
496 renderer.stroke_line(
497 Point::new(cx + 6, cy + 3),
498 Point::new(cx + 3, cy - 3),
499 comp.on_base,
500 2,
501 )?;
502 } else {
503 renderer.stroke_line(
504 Point::new(cx, cy - 3),
505 Point::new(cx + 6, cy - 3),
506 comp.on_base,
507 2,
508 )?;
509 renderer.stroke_line(
510 Point::new(cx, cy - 3),
511 Point::new(cx + 3, cy + 3),
512 comp.on_base,
513 2,
514 )?;
515 renderer.stroke_line(
516 Point::new(cx + 6, cy - 3),
517 Point::new(cx + 3, cy + 3),
518 comp.on_base,
519 2,
520 )?;
521 }
522 Ok(())
523 }
524}
525
526struct DropdownList<'a, C: PixelColor, M: Clone> {
531 rect: Rectangle,
532 base_id: Option<WidgetId>,
533 options: Vec<String>,
534 selected: usize,
535 on_select: Option<Box<dyn Fn(usize) -> M + 'a>>,
536 focused: Option<usize>,
537 pressed: Option<usize>,
539 _color: PhantomData<C>,
540}
541
542impl<C: PixelColor, M: Clone> DropdownList<'_, C, M> {
543 fn list_height(&self) -> u32 {
544 ROW_HEIGHT.saturating_mul(self.options.len() as u32)
545 }
546
547 fn row_at(&self, point: Point) -> Option<usize> {
549 let tl = self.rect.top_left;
550 if point.x < tl.x || point.x >= tl.x + self.rect.size.width as i32 {
551 return None;
552 }
553 let dy = point.y - tl.y;
554 if dy < 0 {
555 return None;
556 }
557 let idx = (dy as u32 / ROW_HEIGHT) as usize;
558 (idx < self.options.len()).then_some(idx)
559 }
560
561 fn row_id(&self, index: usize) -> Option<WidgetId> {
562 self.base_id
563 .map(|id| WidgetId::new(id.raw().wrapping_add(index as u64 + 1)))
564 }
565}
566
567impl<C: PixelColor, M: Clone> Widget<C, M> for DropdownList<'_, C, M> {
568 fn measure(&mut self, constraints: Constraints) -> Size {
569 let w = constraints.max.width;
570 let h = self.list_height().min(constraints.max.height);
571 constraints.clamp(Size::new(w, h))
572 }
573
574 fn preferred_size(&self) -> (Length, Length) {
575 (Length::Fill, Length::Fixed(self.list_height()))
576 }
577
578 fn arrange(&mut self, rect: Rectangle) {
579 self.rect = rect;
580 }
581
582 fn rect(&self) -> Rectangle {
583 self.rect
584 }
585
586 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
587 let row = self.row_at(point);
588 match phase {
589 TouchPhase::Down => {
590 self.pressed = row;
591 None
592 }
593 TouchPhase::Up => {
594 let armed = self.pressed.take();
595 match (row, armed) {
596 (Some(r), Some(a)) if r == a => self.on_select.as_ref().map(|cb| cb(r)),
597 _ => None,
598 }
599 }
600 TouchPhase::Moved => {
601 if row != self.pressed {
602 self.pressed = None;
603 }
604 None
605 }
606 }
607 }
608
609 fn mark_pressed(&mut self, point: Point) {
610 if let Some(r) = self.row_at(point) {
611 self.pressed = Some(r);
612 }
613 }
614
615 fn collect_focusable(&self, out: &mut Vec<WidgetId>) {
616 if self.on_select.is_none() {
617 return;
618 }
619 for index in 0..self.options.len() {
620 if let Some(id) = self.row_id(index) {
621 out.push(id);
622 }
623 }
624 }
625
626 fn sync_focus(&mut self, focused: Option<WidgetId>) {
627 self.focused = focused.and_then(|target| {
628 (0..self.options.len()).find(|index| self.row_id(*index) == Some(target))
629 });
630 }
631
632 fn route_action(&mut self, target: WidgetId, action: UiAction) -> Option<M> {
633 let index = (0..self.options.len()).find(|index| self.row_id(*index) == Some(target))?;
634 match action {
635 UiAction::Activate => self.on_select.as_ref().map(|cb| cb(index)),
636 _ => None,
637 }
638 }
639
640 fn focus_at(&self, point: Point) -> Option<WidgetId> {
641 self.row_at(point).and_then(|index| self.row_id(index))
642 }
643
644 fn focus_rect(&self, target: WidgetId) -> Option<Rectangle> {
645 let index =
646 (0..self.options.len()).find(|candidate| self.row_id(*candidate) == Some(target))?;
647 Some(Rectangle::new(
648 Point::new(
649 self.rect.top_left.x,
650 self.rect.top_left.y + index as i32 * ROW_HEIGHT as i32,
651 ),
652 Size::new(self.rect.size.width, ROW_HEIGHT),
653 ))
654 }
655
656 fn draw<'t>(
657 &self,
658 renderer: &mut dyn Renderer<C>,
659 theme: &Theme<'t, C>,
660 ) -> Result<(), RenderError> {
661 let bg = theme.background.base;
662 let font = theme.default_font();
663 renderer.fill_rect(self.rect, bg)?;
664 renderer.stroke_rect(self.rect, theme.button.border)?;
665
666 let x = self.rect.top_left.x;
667 let w = self.rect.size.width;
668 for (i, opt) in self.options.iter().enumerate() {
669 let y = self.rect.top_left.y + (i as u32 * ROW_HEIGHT) as i32;
670 let row_rect = Rectangle::new(Point::new(x, y), Size::new(w, ROW_HEIGHT));
671
672 let highlighted = self.pressed == Some(i);
673 let selected = i == self.selected;
674 let focused = self.focused == Some(i);
675 if highlighted {
676 renderer.fill_rect(row_rect, theme.accent.pressed)?;
677 } else if selected {
678 renderer.fill_rect(row_rect, theme.accent.base)?;
679 }
680 let border = if focused {
681 theme.accent.base
682 } else {
683 theme.button.border
684 };
685 renderer.stroke_rect(row_rect, border)?;
686
687 let text_color = if highlighted || selected {
688 theme.accent.on_base
689 } else {
690 theme.background.on_base
691 };
692 let text_y = y + ROW_HEIGHT as i32 / 2 + font.character_size.height as i32 / 3;
693 renderer.draw_text(
694 opt,
695 Point::new(x + TEXT_PAD, text_y),
696 font,
697 text_color,
698 Alignment::Left,
699 )?;
700 if i + 1 < self.options.len() && !focused {
701 let sep_y = y + ROW_HEIGHT as i32 - 1;
702 renderer.fill_rect(
703 Rectangle::new(Point::new(x, sep_y), Size::new(w, 1)),
704 theme.background.divider,
705 )?;
706 }
707 }
708 Ok(())
709 }
710}