feat(boot): add enable-logo and enable-animation to the graphical menu (#123)

This commit is contained in:
alex authored and GitHub committed 2026-10-04 14:36:35 -07:00
1 parent afc6d87b91
commit 5be2c1b561
5 files changed
+78 -25

No files matched your search

+3 -3
View File
@@ -630,8 +630,8 @@ fn run(reboot_on_error: &mut bool) -> Result<()> {
.menu_style .menu_style
.unwrap_or(config.options.menu_style); .unwrap_or(config.options.menu_style);
// The graphical menu only uses the mouse when it is enabled in the configuration. // The settings of the graphical menu, which only uses the mouse when it is enabled.
let graphical_mouse = config.menu_styles.graphical.enable_mouse; let graphical = &config.menu_styles.graphical;
// Find the forced boot entry, unless the boot menu is forced. // Find the forced boot entry, unless the boot menu is forced.
// The oneshot entry from the bootloader interface is forced by id, and it is used instead of // The oneshot entry from the bootloader interface is forced by id, and it is used instead of
@@ -672,7 +672,7 @@ fn run(reboot_on_error: &mut bool) -> Result<()> {
menu_timeout, menu_timeout,
menu_disabled, menu_disabled,
menu_style, menu_style,
graphical_mouse, graphical,
&entries, &entries,
) )
.context("unable to select entry via boot menu")?, .context("unable to select entry via boot menu")?,
+5 -3
View File
@@ -6,7 +6,7 @@ use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use core::time::Duration; use core::time::Duration;
use edera_sprout_config::MenuStyle; use edera_sprout_config::{GraphicalMenuConfiguration, MenuStyle};
use eficore::bootloader_interface::BootloaderInterface; use eficore::bootloader_interface::BootloaderInterface;
use eficore::platform::timer::PlatformTimer; use eficore::platform::timer::PlatformTimer;
use log::warn; use log::warn;
@@ -177,7 +177,7 @@ pub fn select<'live>(
timeout: Option<Duration>, timeout: Option<Duration>,
menu_disabled: bool, menu_disabled: bool,
style: MenuStyle, style: MenuStyle,
graphical_mouse: bool, graphical: &GraphicalMenuConfiguration,
entries: &'live [BootableEntry], entries: &'live [BootableEntry],
) -> Result<&'live BootableEntry> { ) -> Result<&'live BootableEntry> {
// A hidden menu gives the user a moment to ask for the menu with a key press. // A hidden menu gives the user a moment to ask for the menu with a key press.
@@ -210,7 +210,9 @@ pub fn select<'live>(
// The graphical menu needs a graphics output, which not every machine has. // The graphical menu needs a graphics output, which not every machine has.
// If it fails, the simple menu is used instead, which falls back to the basic menu. // If it fails, the simple menu is used instead, which falls back to the basic menu.
MenuStyle::Graphical => GraphicalMenu { MenuStyle::Graphical => GraphicalMenu {
enable_mouse: graphical_mouse, enable_mouse: graphical.enable_mouse,
enable_logo: graphical.enable_logo,
enable_animation: graphical.enable_animation,
} }
.select(timeout, entries) .select(timeout, entries)
.or_else(|error| { .or_else(|error| {
+27 -15
View File
@@ -94,6 +94,10 @@ const CURSOR: [&str; 16] = [
pub struct GraphicalMenu { pub struct GraphicalMenu {
/// Whether the mouse is used when the firmware has a pointing device. /// Whether the mouse is used when the firmware has a pointing device.
pub enable_mouse: bool, pub enable_mouse: bool,
/// Whether the logo is drawn next to the name.
pub enable_logo: bool,
/// Whether the logo bounces.
pub enable_animation: bool,
} }
/// The pointing devices of the firmware. /// The pointing devices of the firmware.
@@ -231,8 +235,8 @@ struct Layout {
columns: usize, columns: usize,
/// The row of the line between the entries and the status line. /// The row of the line between the entries and the status line.
footer: usize, footer: usize,
/// The column, row, width and height of the logo when it is on the ground. /// The column, row, width and height of the logo when it is on the ground, if it is shown.
logo: (usize, usize, usize, usize), logo: Option<(usize, usize, usize, usize)>,
/// How many pixels the logo rises while it bounces. /// How many pixels the logo rises while it bounces.
bounce: usize, bounce: usize,
/// The column, row and scale of the name, if there is room for it. /// The column, row and scale of the name, if there is room for it.
@@ -243,7 +247,7 @@ struct Layout {
impl Layout { impl Layout {
/// Calculate the layout of `entries` on a framebuffer of `screen` width and height. /// Calculate the layout of `entries` on a framebuffer of `screen` width and height.
fn new(screen: (usize, usize), entries: &[BootableEntry]) -> Self { fn new(screen: (usize, usize), entries: &[BootableEntry], logo: bool, animate: bool) -> Self {
let (width, height) = screen; let (width, height) = screen;
// Large screens get larger text so that it stays readable, but not so large that the // Large screens get larger text so that it stays readable, but not so large that the
@@ -286,16 +290,18 @@ impl Layout {
(header, visible) = fit(logo_height); (header, visible) = fit(logo_height);
} }
let logo_width = logo_height * LOGO_WIDTH / LOGO_HEIGHT; let logo_width = logo_height * LOGO_WIDTH / LOGO_HEIGHT;
let bounce = logo_height / 16; let bounce = if animate { logo_height / 16 } else { 0 };
// The name is next to the logo, if the two fit together. // The name is next to the logo, if the two fit together.
let title_scale = (logo_height / 32).clamp(scale, scale * 3); let title_scale = (logo_height / 32).clamp(scale, scale * 3);
let title_width = TITLE.len() * GLYPH_SIZE * title_scale; let title_width = TITLE.len() * GLYPH_SIZE * title_scale;
let fits = logo_width + cell + title_width <= width.saturating_sub(margin * 2); // Without the logo, the name is alone and takes the place of the group.
let group = if fits { let shown = if logo { logo_width + cell } else { 0 };
logo_width + cell + title_width let fits = shown + title_width <= width.saturating_sub(margin * 2);
} else { let group = match (logo, fits) {
logo_width (true, true) => shown + title_width,
(true, false) => logo_width,
(false, _) => title_width,
}; };
let group_x = width.saturating_sub(group) / 2; let group_x = width.saturating_sub(group) / 2;
@@ -325,10 +331,10 @@ impl Layout {
text_x: panel_x + padding + cell, text_x: panel_x + padding + cell,
columns: list_width.saturating_sub(cell * 2) / cell, columns: list_width.saturating_sub(cell * 2) / cell,
footer: list_y + list + cell, footer: list_y + list + cell,
logo: (group_x, top + bounce, logo_width, logo_height), logo: logo.then_some((group_x, top + bounce, logo_width, logo_height)),
bounce, bounce,
title: fits.then_some(( title: fits.then_some((
group_x + logo_width + cell, group_x + shown,
top + bounce + logo_height.saturating_sub(GLYPH_SIZE * title_scale) / 2, top + bounce + logo_height.saturating_sub(GLYPH_SIZE * title_scale) / 2,
title_scale, title_scale,
)), )),
@@ -375,7 +381,11 @@ impl Screen {
/// Open every display that the firmware has, or fail if it has none. /// 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 /// 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. /// path, so it is only used if there are no outputs for the displays themselves.
fn open_all(entries: &[BootableEntry]) -> Result<Vec<Self>> { fn open_all(
entries: &[BootableEntry],
enable_logo: bool,
enable_animation: bool,
) -> Result<Vec<Self>> {
let handles = uefi::boot::find_handles::<GraphicsOutput>() let handles = uefi::boot::find_handles::<GraphicsOutput>()
.context("unable to find a graphics output")?; .context("unable to find a graphics output")?;
let has_device_path = |handle: &Handle| { let has_device_path = |handle: &Handle| {
@@ -412,7 +422,7 @@ impl Screen {
) else { ) else {
continue; continue;
}; };
let layout = Layout::new((width, height), entries); let layout = Layout::new((width, height), entries, enable_logo, enable_animation);
render_base(&mut base, &layout); render_base(&mut base, &layout);
screens.push(Self { screens.push(Self {
gop, gop,
@@ -604,7 +614,9 @@ fn draw_centered(
/// Draw the logo above the entries, bouncing a little with each of `ticks`. /// Draw the logo above the entries, bouncing a little with each of `ticks`.
fn draw_logo(fb: &mut Framebuffer, layout: &Layout, ticks: usize) { fn draw_logo(fb: &mut Framebuffer, layout: &Layout, ticks: usize) {
let (x, floor, width, height) = layout.logo; let Some((x, floor, width, height)) = layout.logo else {
return;
};
// The logo rises and falls by up to `bounce` pixels. // The logo rises and falls by up to `bounce` pixels.
let phase = ticks % BOUNCE_TICKS; let phase = ticks % BOUNCE_TICKS;
@@ -930,7 +942,7 @@ impl BootMenu for GraphicalMenu {
return Ok(&entries[default]); return Ok(&entries[default]);
} }
let mut screens = Screen::open_all(entries)?; let mut screens = Screen::open_all(entries, self.enable_logo, self.enable_animation)?;
let mut mouse = self.enable_mouse.then(Mouse::open).flatten(); let mut mouse = self.enable_mouse.then(Mouse::open).flatten();
let result = uefi::system::with_stdin(|input| { let result = uefi::system::with_stdin(|input| {
+36 -1
View File
@@ -111,12 +111,37 @@ pub struct MenuStylesConfiguration {
} }
/// Settings for the graphical boot menu. /// Settings for the graphical boot menu.
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GraphicalMenuConfiguration { pub struct GraphicalMenuConfiguration {
/// Enables selecting entries with the mouse, and shows the cursor. When disabled, or when /// Enables selecting entries with the mouse, and shows the cursor. When disabled, or when
/// the firmware has no pointing device, entries are selected with the keyboard only. /// the firmware has no pointing device, entries are selected with the keyboard only.
#[serde(rename = "enable-mouse", default)] #[serde(rename = "enable-mouse", default)]
pub enable_mouse: bool, pub enable_mouse: bool,
/// Shows the Sprout logo next to the name above the entries. When disabled, only the name is
/// shown.
#[serde(rename = "enable-logo", default = "default_true")]
pub enable_logo: bool,
/// Animates the logo, which bounces. When disabled, the logo stays still.
#[serde(rename = "enable-animation", default = "default_true")]
pub enable_animation: bool,
}
/// The default graphical menu settings, which match an empty graphical section.
impl Default for GraphicalMenuConfiguration {
fn default() -> Self {
Self {
enable_mouse: false,
enable_logo: true,
enable_animation: true,
}
}
}
/// The default for settings that are on unless they are turned off.
fn default_true() -> bool {
true
} }
/// The default configuration, which matches an empty configuration file. /// The default configuration, which matches an empty configuration file.
@@ -203,6 +228,16 @@ mod tests {
assert!(!empty().menu_styles.graphical.enable_mouse); assert!(!empty().menu_styles.graphical.enable_mouse);
} }
#[test]
fn graphical_logo_and_animation_are_on_by_default() {
let graphical = RootConfiguration::default().menu_styles.graphical;
assert!(graphical.enable_logo);
assert!(graphical.enable_animation);
let graphical = empty().menu_styles.graphical;
assert!(graphical.enable_logo);
assert!(graphical.enable_animation);
}
#[test] #[test]
fn bls_strict_mode_is_off_by_default() { fn bls_strict_mode_is_off_by_default() {
assert!(!RootConfiguration::default().options.bls_strict_mode); assert!(!RootConfiguration::default().options.bls_strict_mode);
+7 -3
View File
@@ -142,11 +142,15 @@ Settings for a particular menu style. Only the graphical menu has any.
```toml ```toml
[menu-styles.graphical] [menu-styles.graphical]
enable-mouse = true enable-mouse = true
enable-logo = false
enable-animation = false
``` ```
| Key | Type | Default | Meaning | | Key | Type | Default | Meaning |
|----------------|---------|---------|----------------------------------------------------------------------| |--------------------|---------|---------|------------------------------------------------------------------|
| `enable-mouse` | boolean | `false` | Select entries with the mouse and show the cursor. Without it, or when the firmware has no pointing device, the menu takes the keyboard only. | | `enable-mouse` | boolean | `false` | Select entries with the mouse and show the cursor. Without it, or when the firmware has no pointing device, the menu takes the keyboard only. |
| `enable-logo` | boolean | `true` | Show the Sprout logo next to the name. Without it, only the name is shown. |
| `enable-animation` | boolean | `true` | Bounce the logo. Without it, the logo stays still. |
## `drivers` ## `drivers`