shared/
menu.rs

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}