1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-03-12 22:26:08 +00:00

feat(eckhart): implement bootloader components

- skimmed variants from firmware
This commit is contained in:
obrusvit 2025-03-10 17:59:24 +01:00
parent 300a3bd1a0
commit e0ddc1c596
6 changed files with 682 additions and 8 deletions

View File

@ -0,0 +1,116 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::{Insets, Offset, Rect},
shape::Renderer,
};
use super::super::component::{Button, ButtonMsg};
/// Component for control buttons in the bottom of the screen. Reduced variant
/// for Bootloader UI.
pub struct BldActionBar {
/// Behavior based on `Mode`
mode: Mode,
/// Right or single button.
right_button: Button,
/// Optional left button.
left_button: Option<Button>,
area: Rect,
}
pub enum BldActionBarMsg {
/// Cancel the action
Cancelled,
/// Confirm the action
Confirmed,
}
/// Describes the behavior of the action bar
enum Mode {
/// Single confirm button taking full width
Single,
/// Cancel and confirm button
Double,
}
impl BldActionBar {
pub const ACTION_BAR_HEIGHT: i16 = 90; // [px]
const SPACER_WIDTH: i16 = 4; // [px]
/// TODO: use this offset
/// offset for button content to move it towards center
const BUTTON_CONTENT_OFFSET: Offset = Offset::x(12); // [px]
const BUTTON_EXPAND_TOUCH: Insets = Insets::top(Self::ACTION_BAR_HEIGHT);
/// Create action bar with single button confirming the layout.
pub fn new_single(button: Button) -> Self {
Self::new(
Mode::Single,
None,
button.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH),
)
}
/// Create action bar with cancel and confirm buttons.
pub fn new_double(left: Button, right: Button) -> Self {
Self::new(
Mode::Double,
Some(left.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH)),
right.with_expanded_touch_area(Self::BUTTON_EXPAND_TOUCH),
)
}
fn new(mode: Mode, left_button: Option<Button>, right_button: Button) -> Self {
Self {
mode,
right_button,
left_button,
area: Rect::zero(),
}
}
}
impl Component for BldActionBar {
type Msg = BldActionBarMsg;
fn place(&mut self, bounds: Rect) -> Rect {
debug_assert_eq!(bounds.height(), Self::ACTION_BAR_HEIGHT);
match &self.mode {
Mode::Single => {
self.right_button.place(bounds);
}
Mode::Double => {
let (left, _spacer, right) = bounds.split_center(Self::SPACER_WIDTH);
self.left_button.place(left);
self.right_button.place(right);
}
}
self.area = bounds;
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
match &self.mode {
Mode::Single => {
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
return Some(BldActionBarMsg::Confirmed);
}
}
Mode::Double => {
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
return Some(BldActionBarMsg::Cancelled);
}
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
return Some(BldActionBarMsg::Confirmed);
}
}
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.left_button.render(target);
self.right_button.render(target);
}
}

View File

@ -0,0 +1,184 @@
use crate::{
strutil::TString,
ui::{
component::{Component, Event, EventCtx, Label},
display::{Color, Icon},
geometry::{Alignment2D, Insets, Rect},
shape::{self, Renderer},
},
};
use super::super::{
component::{Button, ButtonContent, ButtonMsg},
constant, theme,
};
const BUTTON_EXPAND_BORDER: i16 = 32;
/// Component for the header of a screen. Reduced variant for Bootloader UI.
pub struct BldHeader<'a> {
area: Rect,
title: Label<'a>,
/// button in the top-right corner
right_button: Option<Button>,
/// button in the top-left corner
left_button: Option<Button>,
right_button_msg: BldHeaderMsg,
left_button_msg: BldHeaderMsg,
/// icon in the top-left corner (used instead of left button)
icon: Option<Icon>,
icon_color: Option<Color>,
}
#[derive(Copy, Clone)]
pub enum BldHeaderMsg {
Back,
Cancelled,
Menu,
Info,
}
impl<'a> BldHeader<'a> {
pub const HEADER_HEIGHT: i16 = theme::HEADER_HEIGHT; // [px]
pub const HEADER_BUTTON_WIDTH: i16 = 56; // [px]
const HEADER_INSETS: Insets = theme::SIDE_INSETS; // [px]
pub const fn new(title: TString<'a>) -> Self {
Self {
area: Rect::zero(),
title: Label::left_aligned(
title,
theme::bootloader::text_title(theme::bootloader::BLD_BG),
)
.vertically_centered(),
right_button: None,
left_button: None,
right_button_msg: BldHeaderMsg::Cancelled,
left_button_msg: BldHeaderMsg::Cancelled,
icon: None,
icon_color: None,
}
}
pub fn new_rsod_header() -> Self {
Self::new("Failure".into()).with_icon(theme::ICON_INFO, theme::RED)
}
pub fn new_pay_attention() -> Self {
Self::new("Pay attention".into()).with_icon(theme::ICON_WARNING, theme::GREY)
}
#[inline(never)]
pub fn with_right_button(self, button: Button, msg: BldHeaderMsg) -> Self {
debug_assert!(matches!(button.content(), ButtonContent::Icon(_)));
let touch_area = Insets::uniform(BUTTON_EXPAND_BORDER);
Self {
right_button: Some(button.with_expanded_touch_area(touch_area)),
right_button_msg: msg,
..self
}
}
#[inline(never)]
pub fn with_left_button(self, button: Button, msg: BldHeaderMsg) -> Self {
debug_assert!(matches!(button.content(), ButtonContent::Icon(_)));
let touch_area = Insets::uniform(BUTTON_EXPAND_BORDER);
Self {
icon: None,
left_button: Some(button.with_expanded_touch_area(touch_area)),
left_button_msg: msg,
..self
}
}
#[inline(never)]
pub fn with_menu_button(self) -> Self {
self.with_right_button(
Button::with_icon(theme::ICON_MENU).styled(theme::bootloader::button_header()),
BldHeaderMsg::Menu,
)
}
#[inline(never)]
pub fn with_close_button(self) -> Self {
self.with_right_button(
Button::with_icon(theme::ICON_CLOSE).styled(theme::bootloader::button_header()),
BldHeaderMsg::Cancelled,
)
}
#[inline(never)]
pub fn with_icon(self, icon: Icon, color: Color) -> Self {
Self {
left_button: None,
icon: Some(icon),
icon_color: Some(color),
..self
}
}
/// Calculates the width needed for the left icon, be it a button with icon
/// or just icon
fn left_icon_width(&self) -> i16 {
let margin_right: i16 = 16; // [px]
if let Some(b) = &self.left_button {
match b.content() {
ButtonContent::Icon(icon) => icon.toif.width() + margin_right,
_ => 0,
}
} else if let Some(icon) = self.icon {
icon.toif.width() + margin_right
} else {
0
}
}
}
impl<'a> Component for BldHeader<'a> {
type Msg = BldHeaderMsg;
fn place(&mut self, bounds: Rect) -> Rect {
debug_assert_eq!(bounds.width(), constant::screen().width());
debug_assert_eq!(bounds.height(), Self::HEADER_HEIGHT);
let bounds = bounds.inset(Self::HEADER_INSETS);
let rest = if let Some(b) = &mut self.right_button {
let (rest, button_area) = bounds.split_right(Self::HEADER_BUTTON_WIDTH);
b.place(button_area);
rest
} else {
bounds
};
let icon_width = self.left_icon_width();
let (rest, title_area) = rest.split_left(icon_width);
self.left_button.place(rest);
self.title.place(title_area);
self.area = bounds;
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
return Some(self.left_button_msg);
};
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
return Some(self.right_button_msg);
};
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.right_button.render(target);
self.left_button.render(target);
if let Some(icon) = self.icon {
shape::ToifImage::new(self.area.left_center(), icon.toif)
.with_fg(self.icon_color.unwrap_or(theme::GREY_LIGHT))
.with_align(Alignment2D::CENTER_LEFT)
.render(target);
}
self.title.render(target);
}
}

View File

@ -0,0 +1,102 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::{Insets, Offset, Rect},
layout_eckhart::component::ButtonMsg,
shape::{Bar, Renderer},
};
use super::super::{component::Button, theme::GREY_EXTRA_DARK};
use heapless::Vec;
pub const MENU_MAX_ITEMS: usize = 3;
type VerticalMenuButtons = Vec<Button, MENU_MAX_ITEMS>;
pub struct BldMenu {
bounds: Rect,
buttons: VerticalMenuButtons,
}
pub enum BldMenuSelectionMsg {
Selected(usize),
}
impl BldMenu {
const SIDE_INSETS: Insets = Insets::sides(24);
pub fn new(buttons: VerticalMenuButtons) -> Self {
Self {
bounds: Rect::zero(),
buttons,
}
}
pub fn empty() -> Self {
Self::new(VerticalMenuButtons::new())
}
pub fn item(mut self, button: Button) -> Self {
unwrap!(self.buttons.push(button));
self
}
fn render_buttons<'s>(&'s self, target: &mut impl Renderer<'s>) {
for button in &self.buttons {
button.render(target);
}
}
fn render_separators<'s>(&'s self, target: &mut impl Renderer<'s>) {
for i in 1..self.buttons.len() {
let button = &self.buttons[i];
let button_prev = &self.buttons[i - 1];
if !button.is_pressed() && !button_prev.is_pressed() {
let separator = Rect::from_top_left_and_size(
button
.area()
.top_left()
.ofs(Offset::x(button.content_offset().x)),
Offset::new(button.area().width() - 2 * button.content_offset().x, 1),
);
Bar::new(separator).with_fg(GREY_EXTRA_DARK).render(target);
}
}
}
}
impl Component for BldMenu {
type Msg = BldMenuSelectionMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// Crop the menu area
self.bounds = bounds.inset(Self::SIDE_INSETS);
let button_width = self.bounds.width();
let mut top_left = self.bounds.top_left();
let padding = 28;
for button in self.buttons.iter_mut() {
let button_height = button.content_height() + 2 * padding;
let button_bounds =
Rect::from_top_left_and_size(top_left, Offset::new(button_width, button_height));
button.place(button_bounds);
top_left = top_left + Offset::y(button_height);
}
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
for (i, button) in self.buttons.iter_mut().enumerate() {
if let Some(ButtonMsg::Clicked) = button.event(ctx, event) {
return Some(Self::Msg::Selected(i));
}
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.render_buttons(target);
self.render_separators(target);
}
}

View File

@ -0,0 +1,90 @@
use crate::ui::{
component::{Component, Event, EventCtx},
geometry::{Alignment, Rect},
layout_eckhart::component::Button,
shape::Renderer,
};
use super::{
super::{
cshape::ScreenBorder,
theme::{bootloader::button_bld_menu, BLUE, HEADER_HEIGHT},
},
bld_menu::BldMenuSelectionMsg,
BldHeader, BldHeaderMsg, BldMenu,
};
const BUTTON_AREA_START: i16 = 56;
const BUTTON_SPACING: i16 = 8;
#[repr(u32)]
#[derive(Copy, Clone, ToPrimitive)]
pub enum BldMenuMsg {
Close = 0xAABBCCDD,
Reboot = 0x11223344,
FactoryReset = 0x55667788,
}
pub struct BldMenuScreen {
header: BldHeader<'static>,
menu: BldMenu,
screen_border: ScreenBorder,
}
impl BldMenuScreen {
pub fn new(firmware_present: bool) -> Self {
let bluetooth = Button::with_text("Bluetooth".into())
.styled(button_bld_menu())
.with_text_align(Alignment::Start)
.initially_enabled(false);
let reboot = Button::with_text("Reboot Trezor".into())
.styled(button_bld_menu())
.with_text_align(Alignment::Start)
.initially_enabled(firmware_present);
let reset = Button::with_text("Factory reset".into())
.styled(button_bld_menu())
.with_text_align(Alignment::Start);
let menu = BldMenu::empty().item(bluetooth).item(reboot).item(reset);
Self {
header: BldHeader::new("Bootloader".into()).with_close_button(),
menu,
screen_border: ScreenBorder::new(BLUE),
}
}
}
impl Component for BldMenuScreen {
type Msg = BldMenuMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let (header_area, menu_area) = bounds.split_top(HEADER_HEIGHT);
self.header.place(header_area);
self.menu.place(menu_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(BldHeaderMsg::Cancelled) = self.header.event(ctx, event) {
return Some(Self::Msg::Close);
}
if let Some(BldMenuSelectionMsg::Selected(n)) = self.menu.event(ctx, event) {
match n {
0 => return Some(Self::Msg::Close),
1 => return Some(Self::Msg::Reboot),
2 => return Some(Self::Msg::FactoryReset),
_ => {}
}
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.header.render(target);
self.menu.render(target);
self.screen_border.render(u8::MAX, target);
}
}

View File

@ -0,0 +1,181 @@
use crate::ui::{
component::{Component, Event, EventCtx, Label},
constant::SCREEN,
geometry::{Insets, Rect},
layout_eckhart::theme::{ACTION_BAR_HEIGHT, SIDE_INSETS},
shape::Renderer,
};
use super::{
super::{
cshape::ScreenBorder,
theme::{HEADER_HEIGHT, TEXT_VERTICAL_SPACING},
},
BldActionBar, BldActionBarMsg, BldHeader, BldHeaderMsg,
};
/// Full-screen component for rendering text. Reduced variant for Bootloader UI.
///
/// The component wraps the full content of the generic page spec:
/// - Header (Optional)
/// - Label with the main text
/// - Label with the secondary text (Optional)
/// - Footer / Action bar (Optional, mutually exclusive)
pub struct BldTextScreen<'a> {
header: Option<BldHeader<'a>>,
label1: Label<'a>,
label2: Option<Label<'a>>,
action_bar: Option<BldActionBar>,
footer: Option<Label<'a>>,
screen_border: Option<ScreenBorder>,
more_info: Option<MoreInfo<'a>>,
more_info_showing: bool,
}
struct MoreInfo<'a> {
header: BldHeader<'a>,
text: Label<'a>,
}
#[derive(Copy, Clone, ToPrimitive)]
pub enum BldTextScreenMsg {
Cancelled = 1,
Confirmed = 2,
Menu = 3,
}
impl<'a> BldTextScreen<'a> {
pub fn new(label1: Label<'a>) -> Self {
Self {
header: None,
label1,
label2: None,
action_bar: None,
footer: None,
screen_border: None,
more_info: None,
more_info_showing: false,
}
}
pub fn with_header(mut self, header: BldHeader<'a>) -> Self {
self.header = Some(header);
self
}
pub fn with_label2(mut self, label2: Label<'a>) -> Self {
self.label2 = Some(label2);
self
}
pub fn with_action_bar(mut self, action_bar: BldActionBar) -> Self {
self.footer = None;
self.action_bar = Some(action_bar);
self
}
pub fn with_footer(mut self, footer: Label<'a>) -> Self {
self.action_bar = None;
self.footer = Some(footer);
self
}
pub fn with_screen_border(mut self, screen_border: ScreenBorder) -> Self {
self.screen_border = Some(screen_border);
self
}
pub fn with_more_info(mut self, header_info: BldHeader<'a>, text_info: Label<'a>) -> Self {
self.more_info = Some(MoreInfo {
header: header_info,
text: text_info,
});
self
}
}
impl<'a> Component for BldTextScreen<'a> {
type Msg = BldTextScreenMsg;
fn place(&mut self, _bounds: Rect) -> Rect {
let (header_area, content_area) = SCREEN.split_top(HEADER_HEIGHT);
let (content_area, action_bar_area) = content_area.split_bottom(ACTION_BAR_HEIGHT);
let content_area = content_area.inset(SIDE_INSETS);
let text1_height = self.label1.text_height(content_area.width());
let text2_height = self
.label2
.as_ref()
.map_or(0, |t| t.text_height(content_area.width()));
let (text1_area, area) = content_area.split_top(text1_height);
let (text2_area, _) = area
.inset(Insets::top(TEXT_VERTICAL_SPACING))
.split_top(text2_height);
self.header.place(header_area);
self.label1.place(text1_area);
self.label2.place(text2_area);
self.action_bar.place(action_bar_area);
self.footer.place(action_bar_area);
if let Some(more_info) = &mut self.more_info {
more_info.header.place(header_area);
more_info.text.place(content_area);
}
SCREEN
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(more_info) = &mut self.more_info {
if self.more_info_showing {
if let Some(BldHeaderMsg::Cancelled) = more_info.header.event(ctx, event) {
self.more_info_showing = false;
return None;
}
}
}
match self.header.event(ctx, event) {
// FIXME: This is a hack for `screen_install_confirm` which expects `2` for the Menu
Some(BldHeaderMsg::Menu) => return Some(BldTextScreenMsg::Cancelled),
Some(BldHeaderMsg::Info) => {
if !self.more_info_showing {
self.more_info_showing = true;
return None;
}
}
_ => (),
}
if let Some(msg) = self.action_bar.event(ctx, event) {
match msg {
BldActionBarMsg::Cancelled => return Some(BldTextScreenMsg::Cancelled),
BldActionBarMsg::Confirmed => return Some(BldTextScreenMsg::Confirmed),
}
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if self.more_info_showing {
if let Some(more_info) = &self.more_info {
more_info.header.render(target);
more_info.text.render(target);
}
} else {
self.header.render(target);
self.label1.render(target);
self.label2.render(target);
self.action_bar.render(target);
self.footer.render(target);
}
if let Some(screen_border) = &self.screen_border {
screen_border.render(u8::MAX, target);
}
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for BldTextScreen<'_> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("BldTextScreen");
}
}

View File

@ -6,19 +6,21 @@ use crate::ui::{
shape::Renderer, shape::Renderer,
}; };
use super::{ use super::super::{
super::theme::{BLACK, GREY, WHITE},
fonts, fonts,
theme::{BLACK, GREY, WHITE},
}; };
const TEXT_ORIGIN: Point = Point::new(0, 105); // TODO: adjust the origin
const STRIDE: i16 = 22; const TEXT_ORIGIN: Point = Point::new(24, 205);
const STRIDE: i16 = 38;
pub struct Welcome { /// Bootloader welcome screen
pub struct BldWelcomeScreen {
bg: Pad, bg: Pad,
} }
impl Welcome { impl BldWelcomeScreen {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
bg: Pad::with_background(BLACK).with_clear(), bg: Pad::with_background(BLACK).with_clear(),
@ -26,7 +28,7 @@ impl Welcome {
} }
} }
impl Component for Welcome { impl Component for BldWelcomeScreen {
type Msg = Never; type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
@ -40,7 +42,6 @@ impl Component for Welcome {
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target); self.bg.render(target);
let font = fonts::FONT_SATOSHI_REGULAR_38; let font = fonts::FONT_SATOSHI_REGULAR_38;
shape::Text::new(TEXT_ORIGIN, "Get started", font) shape::Text::new(TEXT_ORIGIN, "Get started", font)
.with_fg(GREY) .with_fg(GREY)