plex_boot/ui/
boot_menu.rs1use 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
22pub 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 pub fn new(targets: &'a mut [T]) -> Self {
35 Self {
36 targets,
37 selected: 0,
38 }
39 }
40
41 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 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 pub fn wait_for_selection(&mut self, ctx: &mut AppCtx) -> Result<usize, AppError> {
79 loop {
80 self.draw(ctx.display)?;
81
82 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 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}