Skip to main content

plex_boot/ui/
boot_menu.rs

1//! Boot menu interface.
2//!
3//! Renders the list of configured boot targets and handles user input
4//! to select and boot one.
5
6use embedded_graphics::{
7    mono_font::{MonoTextStyle, ascii::FONT_9X15},
8    pixelcolor::Rgb888,
9    prelude::*,
10    primitives::{PrimitiveStyle, Rectangle},
11    text::Text,
12};
13use uefi::proto::console::text::{Key, ScanCode};
14
15use crate::{
16    AppError,
17    core::app::{App, AppCtx, AppResult, DisplayEntry},
18    core::display::GopDisplay,
19    ui::overlay::ErrorOverlay,
20};
21
22/// The main boot menu interface for displaying and selecting boot targets.
23/// Very simple BootMenu that displays listings, handles keyboard input.
24pub struct BootMenu<'a, T>
25where
26    T: App + DisplayEntry,
27{
28    targets: &'a mut [T],
29    selected: usize,
30}
31
32impl<'a, T: App + DisplayEntry> BootMenu<'a, T> {
33    /// Creates a new boot menu to manage the provided list of targets.
34    pub fn new(targets: &'a mut [T]) -> Self {
35        Self {
36            targets,
37            selected: 0,
38        }
39    }
40
41    /// Draws boot options to the buff.
42    pub fn draw(&mut self, display: &mut GopDisplay) -> Result<(), AppError> {
43        display.clear(Rgb888::new(0, 0, 0));
44
45        let text_style = MonoTextStyle::new(&FONT_9X15, Rgb888::WHITE);
46        let selected_text_style = MonoTextStyle::new(&FONT_9X15, Rgb888::BLACK);
47
48        let start_y = 100;
49        let line_height = 25;
50
51        for (i, target) in self.targets.iter().enumerate() {
52            let y = start_y + (i * line_height) as i32;
53            let position = Point::new(50, y);
54            let display_opts = target.display_options();
55
56            if i == self.selected {
57                // draw white bckg to indicate selected
58                let rect = Rectangle::new(Point::new(40, y - 15), Size::new(400, 20));
59                rect.into_styled(PrimitiveStyle::with_fill(Rgb888::WHITE))
60                    .draw(display)
61                    .ok();
62            }
63
64            let this_text_style = if i == self.selected {
65                selected_text_style
66            } else {
67                text_style
68            };
69            Text::new(display_opts.label.as_str(), position, this_text_style)
70                .draw(display)
71                .ok();
72        }
73        display.flush()?;
74        Ok(())
75    }
76
77    /// Handle arrow key input and return the selected index when Enter is pressed.
78    pub fn wait_for_selection(&mut self, ctx: &mut AppCtx) -> Result<usize, AppError> {
79        loop {
80            self.draw(ctx.display)?;
81
82            // unchecked because Option::<NonNull>::None.unwrap_unchecked() == 0
83            // due to the niche optimization with valid size and alignment.
84            let mut events = [unsafe { ctx.input.wait_for_key_event().unwrap_unchecked() }];
85
86            uefi::boot::wait_for_event(&mut events)
87                .map_err(|_| uefi::Error::from(uefi::Status::INVALID_PARAMETER))?;
88
89            // Read the key
90            if let Some(key) = ctx.input.read_key()? {
91                match key {
92                    Key::Special(ScanCode::UP) => {
93                        if self.selected > 0 {
94                            self.selected -= 1;
95                        }
96                    }
97                    Key::Special(ScanCode::DOWN) => {
98                        if self.selected < self.targets.len() - 1 {
99                            self.selected += 1;
100                        }
101                    }
102                    Key::Printable(c) if c == '\r' || c == '\n' => {
103                        return Ok(self.selected);
104                    }
105                    _ => {}
106                }
107            }
108        }
109    }
110}
111
112impl<'a, T: App + DisplayEntry> App for BootMenu<'a, T> {
113    fn run(&mut self, ctx: &mut AppCtx) -> AppResult {
114        loop {
115            let selection = self.wait_for_selection(ctx);
116            let result = match selection {
117                Ok(selection) => {
118                    let bootable = self.targets.get_mut(selection).unwrap();
119                    bootable.run(ctx)
120                }
121
122                Err(e) => {
123                    log::error!("encountered an error in boot menu loop: {e}");
124                    AppResult::Done
125                }
126            };
127
128            match result {
129                AppResult::Done | AppResult::Yield => {
130                    log::info!("returning control flow back to boot menu loop")
131                }
132                AppResult::Booted => {
133                    log::info!("booted target successfully, exiting");
134                    return result;
135                }
136                AppResult::Error(ref err) => {
137                    let mut overlay = ErrorOverlay::new(err);
138                    if let AppResult::Error(_) = overlay.run(ctx) {
139                        log::error!("the error overlay errored, oops.");
140                        return result;
141                    }
142                }
143            }
144        }
145    }
146}