1use core::marker::PhantomData;
2
3use embedded_graphics::{
4 draw_target::{Clipped, DrawTargetExt},
5 mono_font::{
6 MonoTextStyle,
7 ascii::{FONT_6X9, FONT_6X10},
8 },
9 prelude::*,
10 primitives::PrimitiveStyle,
11 text::{Text, renderer::TextRenderer},
12};
13use embedded_graphics_core::{pixelcolor::BinaryColor, primitives::Rectangle};
14
15use crate::held_key::HeldKey;
16
17pub struct Menu<'a, T, F, D>
18where
19 T: AsRef<str> + Clone,
20 F: Fn(
21 &mut Clipped<'_, Clipped<'_, D>>,
22 bool,
23 &str,
24 Point,
25 MonoTextStyle<'_, BinaryColor>,
26 ) -> Option<Point>,
27 D: DrawTarget<Color = BinaryColor> + crate::Keypad,
28{
29 items: &'a mut [T],
30 index: usize,
31 start_of_page_index: usize,
32 bottom_visible_index: usize,
33 page_size: usize,
34 held_key: HeldKey,
35 select_label: Option<&'a str>,
36 renderer: F,
37 display: PhantomData<D>,
38}
39
40impl<'a, T, F, D> Menu<'a, T, F, D>
41where
42 T: AsRef<str> + Clone,
43 F: Fn(
44 &mut Clipped<'_, Clipped<'_, D>>,
45 bool,
46 &str,
47 Point,
48 MonoTextStyle<'_, BinaryColor>,
49 ) -> Option<Point>,
50 D: DrawTarget<Color = BinaryColor> + crate::Keypad,
51{
52 pub fn new(items: &'a mut [T], select_label: Option<&'a str>, renderer: F) -> Self {
53 assert!(!items.is_empty());
54 Self {
55 items,
56 index: 0,
57 start_of_page_index: 0,
58 bottom_visible_index: 0,
59 page_size: 0,
60 held_key: HeldKey::new(750, 250),
61 select_label,
62 renderer,
63 display: PhantomData,
64 }
65 }
66
67 fn down(&mut self) {
68 if self.index < self.items.len() - 1 {
69 self.index += 1;
70 }
71 if self.index > self.bottom_visible_index {
72 self.page_size = self.index - self.start_of_page_index;
73 self.start_of_page_index = self.index;
74 }
75 }
76
77 fn up(&mut self) {
78 if self.index > 0 {
79 self.index -= 1;
80 }
81 if self.index < self.start_of_page_index {
82 self.start_of_page_index = self.index.saturating_sub(self.page_size - 1);
83 }
84 }
85
86 fn text_style(&self, selected: bool) -> MonoTextStyle<'_, BinaryColor> {
87 embedded_graphics::mono_font::MonoTextStyleBuilder::new()
88 .text_color(if selected {
89 BinaryColor::On
90 } else {
91 BinaryColor::Off
92 })
93 .font(&FONT_6X10)
94 .build()
95 }
96
97 fn draw(&mut self, target: &mut D) -> Result<(), ()> {
98 let _ = target
99 .bounding_box()
100 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
101 .draw(target);
102
103 let mut target = if let Some(select_label) = self.select_label {
104 let text_bounding_box = embedded_graphics::mono_font::MonoTextStyleBuilder::new()
105 .text_color(BinaryColor::Off)
106 .font(&FONT_6X9)
107 .build()
108 .measure_string(
109 select_label,
110 Point::new(
111 (target.bounding_box().size.width / 2).try_into().unwrap(),
112 (target.bounding_box().size.height).try_into().unwrap(),
113 ),
114 embedded_graphics::text::Baseline::Bottom,
115 )
116 .bounding_box;
117
118 let text_style = embedded_graphics::text::TextStyleBuilder::new()
119 .alignment(embedded_graphics::text::Alignment::Center)
120 .baseline(embedded_graphics::text::Baseline::Bottom)
121 .build();
122
123 let _ = Text::with_text_style(
124 select_label,
125 Point::new(
126 (target.bounding_box().size.width / 2).try_into().unwrap(),
127 target.bounding_box().size.height as i32 - 1,
128 ),
129 embedded_graphics::mono_font::MonoTextStyleBuilder::new()
130 .text_color(BinaryColor::Off)
131 .font(&FONT_6X9)
132 .build(),
133 text_style,
134 )
135 .draw(target);
136
137 target.clipped(&Rectangle::new(
138 Point::zero(),
139 Size::new(
140 target.bounding_box().size.width,
141 target.bounding_box().size.height - text_bounding_box.size.height,
142 ),
143 ))
144 } else {
145 target.clipped(&Rectangle::new(Point::zero(), target.bounding_box().size))
146 };
147
148 let mut last_drawn_index = 0;
149 let mut point = Point::zero();
150 for (index, a) in self.items[self.start_of_page_index..].iter().enumerate() {
151 if let Some(p) = (self.renderer)(
152 &mut target.clipped(&target.bounding_box()),
153 (self.index - self.start_of_page_index) == index,
154 a.as_ref(),
155 point,
156 self.text_style((self.index - self.start_of_page_index) == index),
157 ) {
158 point = p;
159 last_drawn_index = index;
160 }
161 }
162 self.bottom_visible_index = last_drawn_index + self.start_of_page_index;
163 Ok(())
164 }
165
166 pub async fn process(&mut self, device: &mut D) -> Option<T> {
167 loop {
168 if let Ok(()) = self.draw(device) {
169 match self.held_key.event(device).await {
170 Some(
171 crate::held_key::Event::Down(crate::Key::Down)
172 | crate::held_key::Event::Delay(crate::Key::Down)
173 | crate::held_key::Event::Repeat(crate::Key::Down),
174 ) => {
175 self.down();
176 }
177 Some(
178 crate::held_key::Event::Down(crate::Key::Up)
179 | crate::held_key::Event::Delay(crate::Key::Up)
180 | crate::held_key::Event::Repeat(crate::Key::Up),
181 ) => {
182 self.up();
183 }
184 Some(crate::held_key::Event::Down(crate::Key::Cancel)) => {
185 return None;
186 }
187 Some(crate::held_key::Event::Down(crate::Key::Select)) => {
188 return Some(self.items[self.index].clone());
189 }
190 _ => {}
191 }
192 }
193 }
194 }
195}
196
197pub fn row_render(
198 draw_target: &mut impl DrawTarget<Color = BinaryColor>,
199 selected: bool,
200 item: &str,
201 point: Point,
202 text_style: MonoTextStyle<'_, BinaryColor>,
203) -> Option<Point> {
204 let text_bounding_box = text_style
205 .measure_string(item, point, embedded_graphics::text::Baseline::Top)
206 .bounding_box;
207
208 if draw_target
209 .bounding_box()
210 .contains(Point::new(0, text_bounding_box.bottom_right().unwrap().y))
211 {
212 let _ = Rectangle::new(
213 point,
214 Size::new(
215 draw_target.bounding_box().size.width,
216 text_bounding_box.size.height,
217 ),
218 )
219 .into_styled(PrimitiveStyle::with_fill(if selected {
220 BinaryColor::Off
221 } else {
222 BinaryColor::On
223 }))
224 .draw(draw_target);
225
226 let _ = Text::with_baseline(
227 item,
228 point,
229 text_style,
230 embedded_graphics::text::Baseline::Top,
231 )
232 .draw(draw_target);
233
234 Some(Point::new(
235 point.x,
236 point.y + text_bounding_box.size.height as i32,
237 ))
238 } else {
239 None
240 }
241}
242
243pub fn grid_render(
244 draw_target: &mut impl DrawTarget<Color = BinaryColor>,
245 selected: bool,
246 item: &str,
247 point: Point,
248 text_style: MonoTextStyle<'_, BinaryColor>,
249) -> Option<Point> {
250 let text_bounding_box = text_style
251 .measure_string(item, point, embedded_graphics::text::Baseline::Top)
252 .bounding_box;
253
254 if draw_target.bounding_box().contains(Point::new(
255 text_bounding_box.bottom_right().unwrap().x,
256 text_bounding_box.bottom_right().unwrap().y,
257 )) {
258 let _ = Rectangle::new(
259 point,
260 Size::new(text_bounding_box.size.width, text_bounding_box.size.height),
261 )
262 .into_styled(PrimitiveStyle::with_fill(if selected {
263 BinaryColor::Off
264 } else {
265 BinaryColor::On
266 }))
267 .draw(draw_target);
268
269 let _ = Text::with_baseline(
270 item,
271 point,
272 text_style,
273 embedded_graphics::text::Baseline::Top,
274 )
275 .draw(draw_target);
276
277 Some(Point::new(
278 point.x + text_bounding_box.size.width as i32,
279 point.y,
280 ))
281 } else if draw_target.bounding_box().contains(Point::new(
282 0,
283 text_bounding_box.bottom_right().unwrap().y + text_bounding_box.size.height as i32,
284 )) {
285 let _ = Rectangle::new(
286 Point::new(0, text_bounding_box.bottom_right().unwrap().y),
287 Size::new(text_bounding_box.size.width, text_bounding_box.size.height),
288 )
289 .into_styled(PrimitiveStyle::with_fill(if selected {
290 BinaryColor::Off
291 } else {
292 BinaryColor::On
293 }))
294 .draw(draw_target);
295
296 let _ = Text::with_baseline(
297 item,
298 Point::new(0, text_bounding_box.bottom_right().unwrap().y),
299 text_style,
300 embedded_graphics::text::Baseline::Top,
301 )
302 .draw(draw_target);
303
304 Some(Point::new(
305 text_bounding_box.size.width as i32,
306 text_bounding_box.bottom_right().unwrap().y,
307 ))
308 } else {
309 None
310 }
311}
312
313#[cfg(test)]
314mod test {
315 use embedded_graphics::{Pixel, mock_display::MockDisplay};
316 use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::BinaryColor};
317
318 pub struct Device {
319 pub display: MockDisplay<BinaryColor>,
320 }
321
322 impl Device {
323 pub fn new() -> Self {
324 let mut display = MockDisplay::new();
325 display.set_allow_out_of_bounds_drawing(false);
326 display.set_allow_overdraw(true);
327
328 Self { display }
329 }
330 }
331
332 impl crate::Keypad for Device {
333 async fn event(&mut self) -> crate::KeyEvent {
334 crate::KeyEvent::Down(crate::Key::Down)
335 }
336
337 fn last_pressed(&mut self) -> Option<embassy_time::Duration> {
338 None
339 }
340 }
341
342 impl DrawTarget for Device {
343 type Color = BinaryColor;
344 type Error = core::convert::Infallible;
345 fn draw_iter<
346 I: IntoIterator<Item = Pixel<<Self as embedded_graphics::draw_target::DrawTarget>::Color>>,
347 >(
348 &mut self,
349 pixels: I,
350 ) -> Result<(), <Self as embedded_graphics::draw_target::DrawTarget>::Error> {
351 self.display.draw_iter(pixels)
352 }
353 }
354
355 impl embedded_graphics::prelude::Dimensions for Device {
356 fn bounding_box(&self) -> embedded_graphics::primitives::Rectangle {
357 self.display.bounding_box()
358 }
359 }
360
361 #[test]
362 fn test_row_render() {
363 let mut rows = ["ABC", "XYZ"];
364 let mut device = Device::new();
365 let mut menu = crate::menu::Menu::new(&mut rows, Some("Yyqg"), |a, b, c, d, e| {
366 crate::menu::row_render(a, b, c, d, e)
367 });
368 let _ = menu.draw(&mut device);
369 device.display.assert_pattern(&[
370 "................................................................",
371 "..#...####...###................................................",
372 ".#.#...#..#.#...#...............................................",
373 "#...#..#..#.#...................................................",
374 "#...#..###..#...................................................",
375 "#####..#..#.#...................................................",
376 "#...#..#..#.#...#...............................................",
377 "#...#.####...###................................................",
378 "................................................................",
379 "................................................................",
380 "################################################################",
381 ".###.#.###.#.....###############################################",
382 ".###.#.###.#####.###############################################",
383 "#.#.###.#.#####.################################################",
384 "##.#####.#####.#################################################",
385 "#.#.####.####.##################################################",
386 ".###.###.###.###################################################",
387 ".###.###.###.....###############################################",
388 "################################################################",
389 "################################################################",
390 "################################################################",
391 "################################################################",
392 "################################################################",
393 "################################################################",
394 "################################################################",
395 "################################################################",
396 "################################################################",
397 "################################################################",
398 "################################################################",
399 "################################################################",
400 "################################################################",
401 "################################################################",
402 "################################################################",
403 "################################################################",
404 "################################################################",
405 "################################################################",
406 "################################################################",
407 "################################################################",
408 "################################################################",
409 "################################################################",
410 "################################################################",
411 "################################################################",
412 "################################################################",
413 "################################################################",
414 "################################################################",
415 "################################################################",
416 "################################################################",
417 "################################################################",
418 "################################################################",
419 "################################################################",
420 "################################################################",
421 "################################################################",
422 "################################################################",
423 "################################################################",
424 "################################################################",
425 "################################################################",
426 "#####################.###.######################################",
427 "#####################.###.######################################",
428 "######################.#.###.##.###...###..#####################",
429 "#######################.####.##.##.##.##.##.####################",
430 "#######################.####.##.##.##.##.##.####################",
431 "#######################.#####...###...###...####################",
432 "############################.##.#####.#####.####################",
433 "#############################..######.###..#####################",
434 ]);
435 }
436
437 #[test]
438 fn test_row_render_next() {
439 let mut rows = ["ABC", "XYZ"];
440 let mut device = Device::new();
441 let mut menu = crate::menu::Menu::new(&mut rows, Some("Yyqg"), |a, b, c, d, e| {
442 crate::menu::row_render(a, b, c, d, e)
443 });
444 let _ = menu.draw(&mut device);
445 menu.down();
446 let _ = menu.draw(&mut device);
447 device.display.assert_pattern(&[
448 "################################################################",
449 "##.###....###...################################################",
450 "#.#.###.##.#.###.###############################################",
451 ".###.##.##.#.###################################################",
452 ".###.##...##.###################################################",
453 ".....##.##.#.###################################################",
454 ".###.##.##.#.###.###############################################",
455 ".###.#....###...################################################",
456 "################################################################",
457 "################################################################",
458 "................................................................",
459 "#...#.#...#.#####...............................................",
460 "#...#.#...#.....#...............................................",
461 ".#.#...#.#.....#................................................",
462 "..#.....#.....#.................................................",
463 ".#.#....#....#..................................................",
464 "#...#...#...#...................................................",
465 "#...#...#...#####...............................................",
466 "................................................................",
467 "................................................................",
468 "################################################################",
469 "################################################################",
470 "################################################################",
471 "################################################################",
472 "################################################################",
473 "################################################################",
474 "################################################################",
475 "################################################################",
476 "################################################################",
477 "################################################################",
478 "################################################################",
479 "################################################################",
480 "################################################################",
481 "################################################################",
482 "################################################################",
483 "################################################################",
484 "################################################################",
485 "################################################################",
486 "################################################################",
487 "################################################################",
488 "################################################################",
489 "################################################################",
490 "################################################################",
491 "################################################################",
492 "################################################################",
493 "################################################################",
494 "################################################################",
495 "################################################################",
496 "################################################################",
497 "################################################################",
498 "################################################################",
499 "################################################################",
500 "################################################################",
501 "################################################################",
502 "################################################################",
503 "################################################################",
504 "#####################.###.######################################",
505 "#####################.###.######################################",
506 "######################.#.###.##.###...###..#####################",
507 "#######################.####.##.##.##.##.##.####################",
508 "#######################.####.##.##.##.##.##.####################",
509 "#######################.#####...###...###...####################",
510 "############################.##.#####.#####.####################",
511 "#############################..######.###..#####################",
512 ]);
513 }
514
515 #[test]
516 fn test_grid_render() {
517 let mut rows = [
518 "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
519 ];
520 let mut device = Device::new();
521 let mut menu = crate::menu::Menu::new(&mut rows, Some("Yyqg"), |a, b, c, d, e| {
522 crate::menu::grid_render(a, b, c, d, e)
523 });
524 let _ = menu.draw(&mut device);
525 device.display.assert_pattern(&[
526 "......##########################################################",
527 "..#.......###...##....##.....#.....##...##.###.##...####...#####",
528 ".#.#..#.##.#.###.##.##.#.#####.#####.###.#.###.###.######.######",
529 "#...#.#.##.#.######.##.#.#####.#####.#####.###.###.######.######",
530 "#...#.#...##.######.##.#....##....##.#####.....###.######.######",
531 "#####.#.##.#.######.##.#.#####.#####.##..#.###.###.######.######",
532 "#...#.#.##.#.###.##.##.#.#####.#####.###.#.###.###.###.##.######",
533 "#...#.....###...##....##.....#.######...##.###.##...###..#######",
534 "......##########################################################",
535 "################################################################",
536 ".###.#.#####.###.#.###.#########################################",
537 ".##.##.#####.###.#.###.#########################################",
538 ".#.###.#####..#..#..##.#########################################",
539 "..####.#####.#.#.#.#.#.#########################################",
540 ".#.###.#####.###.#.##..#########################################",
541 ".##.##.#####.###.#.###.#########################################",
542 ".###.#.....#.###.#.###.#########################################",
543 "################################################################",
544 "################################################################",
545 "################################################################",
546 "################################################################",
547 "################################################################",
548 "################################################################",
549 "################################################################",
550 "################################################################",
551 "################################################################",
552 "################################################################",
553 "################################################################",
554 "################################################################",
555 "################################################################",
556 "################################################################",
557 "################################################################",
558 "################################################################",
559 "################################################################",
560 "################################################################",
561 "################################################################",
562 "################################################################",
563 "################################################################",
564 "################################################################",
565 "################################################################",
566 "################################################################",
567 "################################################################",
568 "################################################################",
569 "################################################################",
570 "################################################################",
571 "################################################################",
572 "################################################################",
573 "################################################################",
574 "################################################################",
575 "################################################################",
576 "################################################################",
577 "################################################################",
578 "################################################################",
579 "################################################################",
580 "################################################################",
581 "################################################################",
582 "#####################.###.######################################",
583 "#####################.###.######################################",
584 "######################.#.###.##.###...###..#####################",
585 "#######################.####.##.##.##.##.##.####################",
586 "#######################.####.##.##.##.##.##.####################",
587 "#######################.#####...###...###...####################",
588 "############################.##.#####.#####.####################",
589 "#############################..######.###..#####################",
590 ]);
591 }
592
593 #[test]
594 fn test_grid_render_next() {
595 let mut rows = [
596 "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
597 ];
598 let mut device = Device::new();
599 let mut menu = crate::menu::Menu::new(&mut rows, Some("Yyqg"), |a, b, c, d, e| {
600 crate::menu::grid_render(a, b, c, d, e)
601 });
602 let _ = menu.draw(&mut device);
603 menu.down();
604 let _ = menu.draw(&mut device);
605 menu.down();
606 let _ = menu.draw(&mut device);
607 menu.down();
608 let _ = menu.draw(&mut device);
609 menu.down();
610 let _ = menu.draw(&mut device);
611 menu.down();
612 let _ = menu.draw(&mut device);
613 menu.down();
614 let _ = menu.draw(&mut device);
615 menu.down();
616 let _ = menu.draw(&mut device);
617 menu.down();
618 let _ = menu.draw(&mut device);
619 menu.down();
620 let _ = menu.draw(&mut device);
621 menu.down();
622 let _ = menu.draw(&mut device);
623 menu.down();
624 let _ = menu.draw(&mut device);
625 device.display.assert_pattern(&[
626 "################################################################",
627 "##.###....###...##....##.....#.....##...##.###.##...####...#####",
628 "#.#.###.##.#.###.##.##.#.#####.#####.###.#.###.###.######.######",
629 ".###.##.##.#.######.##.#.#####.#####.#####.###.###.######.######",
630 ".###.##...##.######.##.#....##....##.#####.....###.######.######",
631 ".....##.##.#.######.##.#.#####.#####.##..#.###.###.######.######",
632 ".###.##.##.#.###.##.##.#.#####.#####.###.#.###.###.###.##.######",
633 ".###.#....###...##....##.....#.######...##.###.##...###..#######",
634 "################################################################",
635 "######......####################################################",
636 ".###.##......###.#.###.#########################################",
637 ".##.###......###.#.###.#########################################",
638 ".#.####.......#..#..##.#########################################",
639 "..#####......#.#.#.#.#.#########################################",
640 ".#.####......###.#.##..#########################################",
641 ".##.###......###.#.###.#########################################",
642 ".###.######..###.#.###.#########################################",
643 "######......####################################################",
644 "######......####################################################",
645 "################################################################",
646 "################################################################",
647 "################################################################",
648 "################################################################",
649 "################################################################",
650 "################################################################",
651 "################################################################",
652 "################################################################",
653 "################################################################",
654 "################################################################",
655 "################################################################",
656 "################################################################",
657 "################################################################",
658 "################################################################",
659 "################################################################",
660 "################################################################",
661 "################################################################",
662 "################################################################",
663 "################################################################",
664 "################################################################",
665 "################################################################",
666 "################################################################",
667 "################################################################",
668 "################################################################",
669 "################################################################",
670 "################################################################",
671 "################################################################",
672 "################################################################",
673 "################################################################",
674 "################################################################",
675 "################################################################",
676 "################################################################",
677 "################################################################",
678 "################################################################",
679 "################################################################",
680 "################################################################",
681 "################################################################",
682 "#####################.###.######################################",
683 "#####################.###.######################################",
684 "######################.#.###.##.###...###..#####################",
685 "#######################.####.##.##.##.##.##.####################",
686 "#######################.####.##.##.##.##.##.####################",
687 "#######################.#####...###...###...####################",
688 "############################.##.#####.#####.####################",
689 "#############################..######.###..#####################",
690 ]);
691 }
692}