use crate::entries::BootableEntry; use crate::menu::font::{self, ELLIPSIS, GLYPH_SIZE, Glyph, HEART}; use crate::menu::logo::{LOGO, LOGO_HEIGHT, LOGO_WIDTH}; use crate::menu::{BootMenu, wait_for_events}; use alloc::format; use alloc::string::String; use alloc::vec::Vec; use anyhow::{Context, Result, bail}; use core::time::Duration; use eficore::framebuffer::Framebuffer; use uefi::boot::{OpenProtocolAttributes, OpenProtocolParams, ScopedProtocol}; use uefi::proto::ProtocolPointer; use uefi::proto::console::gop::{BltPixel, GraphicsOutput}; use uefi::proto::console::pointer::{AbsolutePointer, Pointer}; use uefi::proto::console::text::{Input, Key, ScanCode}; use uefi::proto::device_path::DevicePath; use uefi::{Event, Handle}; /// How often the countdown and the logo are updated. const TICK: Duration = Duration::from_millis(125); /// The number of ticks in one bounce of the logo. const BOUNCE_TICKS: usize = 16; /// How many pixels the cursor moves for each millimeter that a relative pointer reports. const PIXELS_PER_MILLIMETER: i64 = 8; /// The title shown above the entries. const TITLE: &str = "sprout"; /// The hint shown once the countdown has been stopped. const HINT: &str = "Pick an entry with the arrow keys or the mouse."; /// The color of the top of the background, which fades into [BACKGROUND_BOTTOM]. const BACKGROUND_TOP: (u8, u8, u8) = (0x3a, 0x1c, 0x4e); /// The color of the bottom of the background. const BACKGROUND_BOTTOM: (u8, u8, u8) = (0x16, 0x0c, 0x24); /// The widest that the status line gets, which the panel has room for. const STATUS_WIDEST: &str = "Boot in 999 s."; /// The color of the panel behind the entries. const PANEL: BltPixel = BltPixel::new(0x22, 0x12, 0x34); /// The color of the edge of the panel, the line below the entries and the scrollbar track. const PANEL_EDGE: BltPixel = BltPixel::new(0x4a, 0x2e, 0x66); /// The color of the pill behind the selected entry, taken from the Sprout logo. const PILL_SELECTED: BltPixel = BltPixel::new(0xff, 0x7a, 0xb8); /// The color of the title and the heart. const ACCENT: BltPixel = BltPixel::new(0xff, 0xa6, 0xd0); /// The color of entries that aren't selected. const TEXT: BltPixel = BltPixel::new(0xf3, 0xd9, 0xea); /// The color of the selected entry. const TEXT_SELECTED: BltPixel = BltPixel::new(0x3a, 0x10, 0x2c); /// The color of the status line. const TEXT_MUTED: BltPixel = BltPixel::new(0xb9, 0x9a, 0xc9); /// The color that the cursor is filled with. const CURSOR_FILL: BltPixel = BltPixel::new(0xff, 0xff, 0xff); /// The color of the outline of the cursor. const CURSOR_OUTLINE: BltPixel = BltPixel::new(0x3a, 0x10, 0x2c); /// The shape of the cursor, where `X` is the outline and `.` is the fill. const CURSOR: [&str; 16] = [ "X ", "XX ", "X.X ", "X..X ", "X...X ", "X....X ", "X.....X ", "X......X ", "X.......X ", "X........X ", "X.....XXXXX", "X..X..X ", "X.X X..X ", "XX X..X ", "X X..X ", " XXXX ", ]; /// A graphical boot menu that selects entries with the keyboard or the mouse. pub struct GraphicalMenu; /// The pointing devices of the firmware. /// Firmware commonly has both kinds of protocol, even if one has no device behind it. struct Mouse { /// A device that reports how far it has moved, like a mouse. relative: Option>, /// A device that reports where it is, like a touch screen or the tablet of a virtual machine. absolute: Option>, } /// Open the protocol `P` on `handle` without taking it from the firmware. /// An exclusive open makes the firmware disconnect every driver that is using the protocol, /// and on some firmware that never returns. fn open_shared(handle: Handle) -> uefi::Result> { // SAFETY: The protocols opened this way are only used from this thread, and the firmware // keeps them installed for as long as the menu is open. unsafe { uefi::boot::open_protocol::

( OpenProtocolParams { handle, agent: uefi::boot::image_handle(), controller: None, }, OpenProtocolAttributes::GetProtocol, ) } } /// Open the pointer protocol `P`, preferring the one on the console input handle. /// That one belongs to the console splitter, which combines every device and is never removed. /// Otherwise, the first device that has the protocol is used. fn open_pointer() -> Option> { // SAFETY: The handle in the system table is either null or a valid handle. let console = uefi::table::system_table_raw() .and_then(|table| unsafe { Handle::from_ptr(table.as_ref().stdin_handle) }); console .and_then(|handle| open_shared::

(handle).ok()) .or_else(|| { uefi::boot::get_handle_for_protocol::

() .and_then(open_shared::

) .ok() }) } impl Mouse { /// Open the pointing devices, if the firmware has any. fn open() -> Option { let relative = open_pointer::(); let absolute = open_pointer::(); (relative.is_some() || absolute.is_some()).then_some(Self { relative, absolute }) } /// The events that are signaled when a device has input. fn events(&self) -> Result> { let mut events = Vec::new(); if let Some(pointer) = &self.relative { events.push(pointer.wait_for_input_event()); } if let Some(pointer) = &self.absolute { events.push(pointer.wait_for_input_event()); } events .into_iter() .collect::>() .context("unable to acquire pointer event") } /// Move `cursor` within `bounds` to where the devices say it is. /// Returns whether the button is held, or [None] if the devices have no new input. fn poll( &mut self, cursor: &mut (usize, usize), bounds: (usize, usize), ) -> Result> { let mut button = None; let max = ( bounds.0.saturating_sub(1) as i64, bounds.1.saturating_sub(1) as i64, ); if let Some(pointer) = &mut self.relative { // The movement is in counts, and the device reports how many are in a millimeter. let mode = *pointer.mode(); if let Some(state) = pointer.read_state().context("unable to read pointer")? { let step = |movement: i32, resolution: u64| { movement as i64 * PIXELS_PER_MILLIMETER / resolution.max(1) as i64 }; let x = cursor.0 as i64 + step(state.relative_movement_x, mode.resolution_x); let y = cursor.1 as i64 + step(state.relative_movement_y, mode.resolution_y); *cursor = (x.clamp(0, max.0) as usize, y.clamp(0, max.1) as usize); button = Some(state.left_button.into()); } } if let Some(pointer) = &mut self.absolute { // The position is somewhere between the minimum and maximum of the device. let mode = *pointer.mode(); if let Some(state) = pointer.read_state().context("unable to read pointer")? { let scale = |value: u64, min: u64, device_max: u64, bound: i64| { let span = device_max.saturating_sub(min).max(1) as i128; (value.saturating_sub(min) as i128 * bound as i128 / span).min(bound as i128) as usize }; *cursor = ( scale( state.current_x, mode.absolute_min_x, mode.absolute_max_x, max.0, ), scale( state.current_y, mode.absolute_min_y, mode.absolute_max_y, max.1, ), ); button = Some(state.active_buttons & 1 != 0); } } Ok(button) } } /// The position and size of everything on the framebuffer. /// The logo and the name are in a header, with a panel below it that holds the entries. struct Layout { /// How many pixels each pixel of the font is drawn as. scale: usize, /// The size of a character in pixels. cell: usize, /// The column that entries start at. x: usize, /// The row that the first entry starts at. y: usize, /// The width of each entry. width: usize, /// The height of each entry. height: usize, /// The space between entries. gap: usize, /// The number of entries that fit on the framebuffer. visible: usize, /// The column, row, width and height of the panel behind the entries. panel: (usize, usize, usize, usize), /// How round the corners of the panel are. radius: usize, /// The column that the text of an entry starts at. text_x: usize, /// How many characters of a title fit in an entry. columns: usize, /// The row of the line between the entries and the status line. footer: usize, /// The column, row, width and height of the logo when it is on the ground. logo: (usize, usize, usize, usize), /// How many pixels the logo rises while it bounces. bounce: usize, /// The column, row and scale of the name, if there is room for it. title: Option<(usize, usize, usize)>, /// Whether there are more entries than fit, which adds a scrollbar. scrolls: bool, } impl Layout { /// Calculate the layout of `entries` on a framebuffer of `screen` width and height. fn new(screen: (usize, usize), entries: &[BootableEntry]) -> Self { let (width, height) = screen; // Large screens get larger text so that it stays readable, but not so large that the // titles take up the whole screen. let scale = (height / 540).min(width / 640).clamp(1, 3); let cell = GLYPH_SIZE * scale; let margin = (cell * 2).max(height / 20).min(width / 8); let padding = cell * 3 / 2; let (row, gap) = (cell * 2, scale * 2); let header_gap = cell * 2; // The line between the entries and the status, and the status below it. let footer_height = cell + scale + cell / 2 + cell; // The panel is the width of the longest title, but never most of the screen. let longest = entries .iter() .map(|entry| entry.title().chars().count()) .max() .unwrap_or(0); let status = HINT.len().max(STATUS_WIDEST.len()); let panel_width = ((longest + 4) * cell + padding * 2) .max(40 * cell) .max((status + 8) * cell) .min(width.saturating_sub(margin * 2).min(width * 3 / 5)); // The header is as tall as the logo, and the logo needs room to bounce. let fit = |logo: usize| { let header = logo + logo / 16; let space = height .saturating_sub(margin * 2 + header + header_gap + padding * 2 + footer_height); let visible = ((space + gap) / (row + gap)).clamp(1, entries.len().max(1)); (header, visible) }; let mut logo_height = (LOGO_HEIGHT * 2).min(height / 7).max(32); let (mut header, mut visible) = fit(logo_height); // A short screen gets a smaller logo before it loses entries. if visible < entries.len().min(3) { logo_height = (logo_height / 2).max(32); (header, visible) = fit(logo_height); } let logo_width = logo_height * LOGO_WIDTH / LOGO_HEIGHT; let bounce = logo_height / 16; // The name is next to the logo, if the two fit together. let title_scale = (logo_height / 32).clamp(scale, scale * 3); let title_width = TITLE.len() * GLYPH_SIZE * title_scale; let fits = logo_width + cell + title_width <= width.saturating_sub(margin * 2); let group = if fits { logo_width + cell + title_width } else { logo_width }; let group_x = width.saturating_sub(group) / 2; let list = visible * (row + gap) - gap; let panel_height = padding * 2 + list + footer_height; let top = (height.saturating_sub(header + header_gap + panel_height) / 2).max(margin); let panel_x = width.saturating_sub(panel_width) / 2; let panel_y = top + header + header_gap; // The scrollbar is in a column next to the entries. let scrolls = visible < entries.len(); let gutter = if scrolls { cell } else { 0 }; let list_width = panel_width.saturating_sub(padding * 2 + gutter); let list_y = panel_y + padding; Self { scale, cell, x: panel_x + padding, y: list_y, width: list_width, height: row, gap, visible, panel: (panel_x, panel_y, panel_width, panel_height), radius: cell, text_x: panel_x + padding + cell * 3, columns: list_width.saturating_sub(cell * 4) / cell, footer: list_y + list + cell, logo: (group_x, top + bounce, logo_width, logo_height), bounce, title: fits.then_some(( group_x + logo_width + cell, top + bounce + logo_height.saturating_sub(GLYPH_SIZE * title_scale) / 2, title_scale, )), scrolls, } } /// The row that the entry at `slot` on the screen starts at. fn row(&self, slot: usize) -> usize { self.y + slot * (self.height + self.gap) } /// The slot of the entry that is at `position`, if there is one. fn slot_at(&self, position: (usize, usize)) -> Option { let (x, y) = ( position.0.checked_sub(self.x)?, position.1.checked_sub(self.y)?, ); let slot = y / (self.height + self.gap); (x < self.width && y % (self.height + self.gap) < self.height && slot < self.visible) .then_some(slot) } } /// A display that the menu is drawn to. struct Screen { /// The graphics output of the display. gop: ScopedProtocol, /// What is drawn before it is blitted to the display. fb: Framebuffer, /// Where the entries are on this display. layout: Layout, /// The index of the first entry that is on this display. offset: usize, } impl Screen { /// Open every display that the firmware has, or fail if it has none. /// The console splitter has a graphics output that draws to every display. It has no device /// path, so it is only used if there are no outputs for the displays themselves. fn open_all(entries: &[BootableEntry]) -> Result> { let handles = uefi::boot::find_handles::() .context("unable to find a graphics output")?; let has_device_path = |handle: &Handle| { uefi::boot::test_protocol::(OpenProtocolParams { handle: *handle, agent: uefi::boot::image_handle(), controller: None, }) .unwrap_or(false) }; let displays = handles .iter() .copied() .filter(has_device_path) .collect::>(); let handles = if displays.is_empty() { handles } else { displays }; let mut screens = Vec::new(); for handle in handles { // Opening it exclusively would disconnect the firmware's text console from the // display, and anything printed after the menu may not show up. let Ok(gop) = open_shared::(handle) else { continue; }; let (width, height) = gop.current_mode_info().resolution(); let Ok(fb) = Framebuffer::new(width, height) else { continue; }; let layout = Layout::new((width, height), entries); screens.push(Self { gop, fb, layout, offset: 0, }); } if screens.is_empty() { bail!("unable to open a graphics output"); } Ok(screens) } /// The size of the display in pixels. fn size(&self) -> (usize, usize) { (self.fb.width(), self.fb.height()) } /// Where `position` on a display of size `from` is on this display, keeping its place /// relative to the edges. fn locate(&self, position: (usize, usize), from: (usize, usize)) -> (usize, usize) { let scale = |value: usize, to: usize, from: usize| { (value * to / from.max(1)).min(to.saturating_sub(1)) }; ( scale(position.0, self.fb.width(), from.0), scale(position.1, self.fb.height(), from.1), ) } /// Scroll so that the `selected` entry is visible. fn scroll_to(&mut self, selected: usize) { if selected < self.offset { self.offset = selected; } else if selected >= self.offset + self.layout.visible { self.offset = selected + 1 - self.layout.visible; } } } /// What is shown on the framebuffer. struct Scene<'a> { /// The entries that can be chosen. entries: &'a [BootableEntry], /// The index of the selected entry. selected: usize, /// The status line at the bottom of the screen. status: String, /// The number of ticks since the menu was shown, which moves the logo. ticks: usize, } /// Fill `rows` with a gradient from `top` to `bottom`. fn draw_background(fb: &mut Framebuffer, top: (u8, u8, u8), bottom: (u8, u8, u8)) { let (width, height) = (fb.width(), fb.height()); let mix = |from: u8, to: u8, row: usize| { (from as i64 + (to as i64 - from as i64) * row as i64 / height.max(1) as i64) as u8 }; for row in 0..height { let color = BltPixel::new( mix(top.0, bottom.0, row), mix(top.1, bottom.1, row), mix(top.2, bottom.2, row), ); fb.fill_rect(0, row, width, 1, color); } } /// Fill a rectangle that has rounded corners of `radius`. fn draw_rounded( fb: &mut Framebuffer, x: usize, y: usize, width: usize, height: usize, radius: usize, color: BltPixel, ) { if width == 0 || height == 0 { return; } let radius = radius.min(height / 2).min(width / 2); for row in 0..height { // How far the row is from the edge, where the corners curve in. let edge = row.min(height - 1 - row); let inset = if edge >= radius { 0 } else { let rise = radius - edge; (0..radius) .find(|inset| (radius - inset).pow(2) + rise.pow(2) <= radius.pow(2)) .unwrap_or(radius) }; fb.fill_rect( x + inset, y + row, width.saturating_sub(inset * 2), 1, color, ); } } /// Fill a rectangle with the shape of a pill, which has fully rounded ends. fn draw_pill( fb: &mut Framebuffer, x: usize, y: usize, width: usize, height: usize, color: BltPixel, ) { draw_rounded(fb, x, y, width, height, height / 2, color); } /// Draw `glyph` at `x` and `y`, with each pixel of it drawn as `scale` by `scale` pixels. fn draw_glyph( fb: &mut Framebuffer, x: usize, y: usize, scale: usize, glyph: &Glyph, color: BltPixel, ) { for (row, bits) in glyph.iter().enumerate() { for column in (0..GLYPH_SIZE).filter(|column| bits >> column & 1 != 0) { fb.fill_rect(x + column * scale, y + row * scale, scale, scale, color); } } } /// Draw `text` starting at `x` and `y`, cut off with an ellipsis if it is over `columns` characters. fn draw_text( fb: &mut Framebuffer, x: usize, y: usize, scale: usize, columns: usize, text: &str, color: BltPixel, ) { let cut = text.chars().count() > columns; let shown = if cut { columns.saturating_sub(1) } else { columns }; for (index, c) in text.chars().take(shown).enumerate() { draw_glyph( fb, x + index * GLYPH_SIZE * scale, y, scale, font::glyph(c), color, ); } if cut && columns > 0 { draw_glyph( fb, x + shown * GLYPH_SIZE * scale, y, scale, &ELLIPSIS, color, ); } } /// Draw `text` in the middle of `x` and `width` at `y`. fn draw_centered( fb: &mut Framebuffer, x: usize, width: usize, y: usize, scale: usize, text: &str, color: BltPixel, ) { let columns = width / (GLYPH_SIZE * scale); let used = text.chars().count().min(columns) * GLYPH_SIZE * scale; draw_text(fb, x + (width - used) / 2, y, scale, columns, text, color); } /// Draw the logo above the entries, bouncing a little with each of `ticks`. fn draw_logo(fb: &mut Framebuffer, layout: &Layout, ticks: usize) { let (x, floor, width, height) = layout.logo; // The logo rises and falls by up to `bounce` pixels. let phase = ticks % BOUNCE_TICKS; let rise = phase.min(BOUNCE_TICKS - phase) * layout.bounce / (BOUNCE_TICKS / 2); let y = floor - rise; for row in 0..height { for column in 0..width { let source = ((row * LOGO_HEIGHT / height) * LOGO_WIDTH + column * LOGO_WIDTH / width) * 4; let [red, green, blue, alpha] = LOGO[source..source + 4] else { continue; }; if alpha == 0 { continue; } // The channels are premultiplied, so the color is the logo plus what shows through. if let Some(pixel) = fb.pixel(x + column, y + row) { let through = 255 - alpha as u32; let blend = |logo: u8, behind: u8| (logo as u32 + behind as u32 * through / 255) as u8; *pixel = BltPixel::new( blend(red, pixel.red), blend(green, pixel.green), blend(blue, pixel.blue), ); } } } } /// Draw the track and the thumb of the scrollbar, which shows where `offset` is in `total`. fn draw_scrollbar(fb: &mut Framebuffer, layout: &Layout, offset: usize, total: usize) { let height = layout.visible * (layout.height + layout.gap) - layout.gap; let x = layout.x + layout.width + layout.cell / 2 - layout.scale; let width = layout.scale * 2; fb.fill_rect(x, layout.y, width, height, PANEL_EDGE); let thumb = (height * layout.visible / total) .max(layout.cell) .min(height); let travel = total.saturating_sub(layout.visible).max(1); let y = layout.y + (height - thumb) * offset.min(travel) / travel; draw_rounded(fb, x, y, width, thumb, width / 2, ACCENT); } /// Draw the cursor with its tip at `position`. fn draw_cursor(fb: &mut Framebuffer, scale: usize, position: (usize, usize)) { for (row, line) in CURSOR.iter().enumerate() { for (column, shape) in line.bytes().enumerate() { let color = match shape { b'X' => CURSOR_OUTLINE, b'.' => CURSOR_FILL, _ => continue, }; fb.fill_rect( position.0 + column * scale, position.1 + row * scale, scale, scale, color, ); } } } /// Draw the whole menu to the framebuffer. fn render( fb: &mut Framebuffer, layout: &Layout, scene: &Scene, offset: usize, cursor: Option<(usize, usize)>, ) { draw_background(fb, BACKGROUND_TOP, BACKGROUND_BOTTOM); draw_logo(fb, layout, scene.ticks); if let Some((x, y, scale)) = layout.title { draw_text(fb, x, y, scale, TITLE.len(), TITLE, ACCENT); } let (panel_x, panel_y, panel_width, panel_height) = layout.panel; let edge = layout.scale; draw_rounded( fb, panel_x, panel_y, panel_width, panel_height, layout.radius, PANEL_EDGE, ); draw_rounded( fb, panel_x + edge, panel_y + edge, panel_width.saturating_sub(edge * 2), panel_height.saturating_sub(edge * 2), layout.radius.saturating_sub(edge), PANEL, ); for (slot, (index, entry)) in scene .entries .iter() .enumerate() .skip(offset) .take(layout.visible) .enumerate() { let row = layout.row(slot); let text_row = row + (layout.height - layout.cell) / 2; // Only the selected entry has a pill, which keeps the list calm. let text = if index == scene.selected { draw_pill( fb, layout.x, row, layout.width, layout.height, PILL_SELECTED, ); draw_glyph( fb, layout.x + layout.cell, text_row, layout.scale, &HEART, TEXT_SELECTED, ); TEXT_SELECTED } else { TEXT }; draw_text( fb, layout.text_x, text_row, layout.scale, layout.columns, entry.title(), text, ); } if layout.scrolls { draw_scrollbar(fb, layout, offset, scene.entries.len()); } // The status line is below a line across the panel, with the position when it scrolls. let inner = panel_width.saturating_sub(layout.cell * 3); let inner_x = panel_x + layout.cell * 3 / 2; fb.fill_rect(inner_x, layout.footer, inner, layout.scale, PANEL_EDGE); let status_row = layout.footer + layout.scale + layout.cell / 2; // The position is at the right edge, so the status keeps clear of it on both sides. let keep_clear = if layout.scrolls { layout.cell * 8 } else { 0 }; draw_centered( fb, inner_x + keep_clear, inner.saturating_sub(keep_clear * 2), status_row, layout.scale, &scene.status, TEXT_MUTED, ); if layout.scrolls { let position = format!("{}/{}", scene.selected + 1, scene.entries.len()); let width = position.len() * layout.cell; draw_text( fb, (inner_x + inner).saturating_sub(width), status_row, layout.scale, position.len(), &position, TEXT_MUTED, ); } if let Some(position) = cursor { draw_cursor(fb, layout.scale, position); } } /// Run the menu until an entry is chosen, returning the index of that entry. /// The `selected` entry is booted if there is no input before `timeout` passes. /// Without a timeout, the menu waits for the user. fn run( input: &mut Input, mut mouse: Option, screens: &mut [Screen], timeout: Option, entries: &[BootableEntry], selected: usize, ) -> Result { // The cursor moves within the first screen, and is shown at the same place on the others. let bounds = screens[0].size(); let page = screens[0].layout.visible; let last = entries.len() - 1; // The events that wake the menu up, which are keys and pointer input. let mut events = Vec::from([input .wait_for_key_event() .context("unable to acquire key event")?]); if let Some(mouse) = &mouse { events.extend(mouse.events()?); } let mut scene = Scene { entries, selected, status: String::new(), ticks: 0, }; let mut cursor = mouse.as_ref().map(|_| (bounds.0 / 2, bounds.1 / 2)); // The time left before booting, or None once input stops the countdown. let mut remaining = timeout; let mut button_down = false; loop { scene.status = match remaining { Some(remaining) => format!("Boot in {} s.", remaining.as_millis().div_ceil(1000)), None => String::from(HINT), }; for screen in screens.iter_mut() { screen.scroll_to(scene.selected); let cursor = cursor.map(|position| screen.locate(position, bounds)); render( &mut screen.fb, &screen.layout, &scene, screen.offset, cursor, ); screen.fb.blit(&mut screen.gop)?; } // Wake up every tick to move the logo and update the countdown. if wait_for_events(&events, Some(TICK))?.is_none() { scene.ticks += 1; if let Some(left) = remaining { let left = left.saturating_sub(TICK); if left.is_zero() { return Ok(scene.selected); } remaining = Some(left); } continue; } // Some firmware signals an event without any input being available. if let Some(key) = input.read_key().context("unable to read key")? { remaining = None; match key { Key::Special(ScanCode::UP) => scene.selected = scene.selected.saturating_sub(1), Key::Special(ScanCode::DOWN) => scene.selected = (scene.selected + 1).min(last), Key::Special(ScanCode::HOME) => scene.selected = 0, Key::Special(ScanCode::END) => scene.selected = last, Key::Special(ScanCode::PAGE_UP) => { scene.selected = scene.selected.saturating_sub(page) } Key::Special(ScanCode::PAGE_DOWN) => { scene.selected = (scene.selected + page).min(last) } // Serial consoles may send a line feed instead of a carriage return. Key::Printable(c) if matches!(char::from(c), '\r' | '\n') => { return Ok(scene.selected); } _ => {} } } if let (Some(mouse), Some(cursor)) = (mouse.as_mut(), cursor.as_mut()) && let Some(down) = mouse.poll(cursor, bounds)? { remaining = None; // Hovering over an entry selects it, and pressing the button boots it. let hovered = screens.iter().find_map(|screen| { let position = screen.locate(*cursor, bounds); screen .layout .slot_at(position) .map(|slot| screen.offset + slot) }); if let Some(entry) = hovered { scene.selected = entry; if down && !button_down { return Ok(scene.selected); } } button_down = down; } } } impl BootMenu for GraphicalMenu { /// Selects an entry using the graphical menu. /// The menu is drawn to every display, and fails if the firmware has none. fn select<'a>( &self, timeout: Option, entries: &'a [BootableEntry], ) -> Result<&'a BootableEntry> { let default = entries .iter() .position(|entry| entry.is_default()) .context("no default entry available")?; // If the timeout is zero, boot the default entry without showing the menu. if timeout.is_some_and(|timeout| timeout.is_zero()) { return Ok(&entries[default]); } let mut screens = Screen::open_all(entries)?; let mut mouse = Mouse::open(); let result = uefi::system::with_stdin(|input| { run(input, mouse.take(), &mut screens, timeout, entries, default) }); // Clear the console so that anything printed after the menu is readable. let _ = uefi::system::with_stdout(|output| output.clear()); Ok(&entries[result?]) } }