Skip to main content

plex_boot/ui/
overlay.rs

1//! UI overlays for displaying messages and errors.
2//!
3//! Provides reusable graphical overlays that can be drawn on top of
4//! the current screen, such as error dialogs.
5
6use crate::AppError;
7use crate::core::app::{App, AppCtx, AppResult};
8use alloc::string::{String, ToString as _};
9use alloc::vec::Vec;
10use embedded_graphics::{
11    mono_font::{MonoTextStyle, ascii::FONT_9X15},
12    pixelcolor::Rgb888,
13    prelude::*,
14    primitives::{PrimitiveStyleBuilder, Rectangle},
15    text::Text,
16};
17use uefi::proto::console::text::{Key, ScanCode};
18
19/// An error overlay that displays an application error and waits for the user
20/// to acknowledge it before dismissing.
21pub struct ErrorOverlay<'a> {
22    error: &'a AppError,
23}
24
25impl<'a> ErrorOverlay<'a> {
26    /// Creates a new error overlay.
27    pub fn new(error: &'a AppError) -> Self {
28        Self { error }
29    }
30}
31
32impl<'a> App for ErrorOverlay<'a> {
33    fn run(&mut self, ctx: &mut AppCtx) -> AppResult {
34        let text = alloc::format!("{}", self.error);
35        draw_error_overlay(ctx, &text);
36
37        if let Err(err) = ctx.display.flush() {
38            return AppResult::Error(err.into());
39        }
40
41        loop {
42            let mut events = [unsafe { ctx.input.wait_for_key_event().unwrap_unchecked() }];
43
44            if uefi::boot::wait_for_event(&mut events).is_err() {
45                return AppResult::Error(uefi::Status::INVALID_PARAMETER.into());
46            }
47
48            if let Ok(Some(key)) = ctx.input.read_key() {
49                if matches!(key, Key::Printable(c) if c == '\r' || c == '\n') {
50                    return AppResult::Done;
51                }
52                if matches!(key, Key::Special(ScanCode::END)) {
53                    return AppResult::Done;
54                }
55            }
56        }
57    }
58}
59
60fn draw_error_overlay(ctx: &mut AppCtx, text: &str) {
61    let size = ctx.display.size();
62    let screen_w = size.width as i32;
63    let screen_h = size.height as i32;
64    let box_w = (screen_w * 2 / 3).max(280);
65    let box_h = (screen_h / 3).max(120);
66    let left = (screen_w - box_w) / 2;
67    let top = (screen_h - box_h) / 2;
68
69    let background = PrimitiveStyleBuilder::new()
70        .fill_color(Rgb888::new(20, 20, 20))
71        .stroke_color(Rgb888::new(220, 220, 220))
72        .stroke_width(2)
73        .build();
74    Rectangle::new(Point::new(left, top), Size::new(box_w as u32, box_h as u32))
75        .into_styled(background)
76        .draw(ctx.display)
77        .ok();
78
79    let title_style = MonoTextStyle::new(&FONT_9X15, Rgb888::new(255, 80, 80));
80    let body_style = MonoTextStyle::new(&FONT_9X15, Rgb888::WHITE);
81
82    let padding_x = 12;
83    let padding_y = 16;
84    let line_height = 18;
85    let max_chars = ((box_w - padding_x * 2) / 9).max(1) as usize;
86    let max_lines = ((box_h - padding_y * 2) / line_height).max(1) as usize;
87
88    Text::new(
89        "Error",
90        Point::new(left + padding_x, top + padding_y),
91        title_style,
92    )
93    .draw(ctx.display)
94    .ok();
95
96    let lines = wrap_lines(text, max_chars, max_lines.saturating_sub(1));
97    for (idx, line) in lines.iter().enumerate() {
98        let y = top + padding_y + line_height * (idx as i32 + 1);
99        Text::new(line.as_str(), Point::new(left + padding_x, y), body_style)
100            .draw(ctx.display)
101            .ok();
102    }
103}
104
105fn wrap_lines(text: &str, max_chars: usize, max_lines: usize) -> Vec<String> {
106    let mut lines = Vec::new();
107    for raw in text.lines() {
108        let mut start = 0;
109        let bytes = raw.as_bytes();
110        while start < bytes.len() {
111            let end = (start + max_chars).min(bytes.len());
112            let slice = &raw[start..end];
113            lines.push(slice.to_string());
114            start = end;
115            if lines.len() >= max_lines {
116                return lines;
117            }
118        }
119        if lines.len() >= max_lines {
120            return lines;
121        }
122    }
123    lines
124}