mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-03-13 06:36:06 +00:00
feat(eckhart): implement bootloader components
- skimmed variants of Header, ActionBar, TextScreen, Menu - implement ErrorScreen - implement WelcomeScreen - implement bootloader loader
This commit is contained in:
parent
7197fd5964
commit
b5fdbde99e
@ -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);
|
||||
}
|
||||
}
|
184
core/embed/rust/src/ui/layout_eckhart/bootloader/bld_header.rs
Normal file
184
core/embed/rust/src/ui/layout_eckhart/bootloader/bld_header.rs
Normal 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);
|
||||
}
|
||||
}
|
100
core/embed/rust/src/ui/layout_eckhart/bootloader/bld_menu.rs
Normal file
100
core/embed/rust/src/ui/layout_eckhart/bootloader/bld_menu.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Offset, Rect},
|
||||
layout_eckhart::{component::ButtonMsg, theme},
|
||||
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 {
|
||||
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(theme::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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -6,19 +6,21 @@ use crate::ui::{
|
||||
shape::Renderer,
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::theme::{BLACK, GREY, WHITE},
|
||||
use super::super::{
|
||||
fonts,
|
||||
theme::{BLACK, GREY, WHITE},
|
||||
};
|
||||
|
||||
const TEXT_ORIGIN: Point = Point::new(0, 105);
|
||||
const STRIDE: i16 = 22;
|
||||
// TODO: adjust the origin
|
||||
const TEXT_ORIGIN: Point = Point::new(24, 205);
|
||||
const STRIDE: i16 = 38;
|
||||
|
||||
pub struct Welcome {
|
||||
/// Bootloader welcome screen
|
||||
pub struct BldWelcomeScreen {
|
||||
bg: Pad,
|
||||
}
|
||||
|
||||
impl Welcome {
|
||||
impl BldWelcomeScreen {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
bg: Pad::with_background(BLACK).with_clear(),
|
||||
@ -26,7 +28,7 @@ impl Welcome {
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Welcome {
|
||||
impl Component for BldWelcomeScreen {
|
||||
type Msg = Never;
|
||||
|
||||
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>) {
|
||||
self.bg.render(target);
|
||||
|
||||
let font = fonts::FONT_SATOSHI_REGULAR_38;
|
||||
shape::Text::new(TEXT_ORIGIN, "Get started", font)
|
||||
.with_fg(GREY)
|
@ -1,52 +1,44 @@
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx, Label, Never, Pad},
|
||||
constant::screen,
|
||||
geometry::{Alignment2D, Point, Rect},
|
||||
shape,
|
||||
component::{Component, Event, EventCtx, Label, Never},
|
||||
constant::SCREEN,
|
||||
geometry::{Insets, Rect},
|
||||
shape::Renderer,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
constant::WIDTH,
|
||||
theme::{FATAL_ERROR_COLOR, ICON_WARNING40, RESULT_FOOTER_START, RESULT_PADDING, WHITE},
|
||||
use super::super::{
|
||||
cshape::ScreenBorder,
|
||||
theme::{
|
||||
ACTION_BAR_HEIGHT, HEADER_HEIGHT, RED, SIDE_INSETS, TEXT_NORMAL, TEXT_SMALL,
|
||||
TEXT_SMALL_GREY, TEXT_SMALL_RED, TEXT_VERTICAL_SPACING,
|
||||
},
|
||||
ResultFooter, ResultStyle,
|
||||
};
|
||||
|
||||
const ICON_TOP: i16 = 23;
|
||||
const TITLE_AREA_START: i16 = 70;
|
||||
const MESSAGE_AREA_START: i16 = 90;
|
||||
|
||||
#[cfg(feature = "bootloader")]
|
||||
const STYLE: &ResultStyle = &crate::ui::layout_eckhart::theme::bootloader::RESULT_WIPE;
|
||||
#[cfg(not(feature = "bootloader"))]
|
||||
const STYLE: &ResultStyle = &super::super::theme::RESULT_ERROR;
|
||||
|
||||
/// Full-screen component showing Eckhart RSOD. To keep it minimal, this screen
|
||||
/// does not use any other components.
|
||||
pub struct ErrorScreen<'a> {
|
||||
bg: Pad,
|
||||
header: Label<'a>,
|
||||
title: Label<'a>,
|
||||
message: Label<'a>,
|
||||
footer: ResultFooter<'a>,
|
||||
footer: Label<'a>,
|
||||
screen_border: ScreenBorder,
|
||||
}
|
||||
|
||||
impl<'a> ErrorScreen<'a> {
|
||||
pub fn new(title: TString<'a>, message: TString<'a>, footer: TString<'a>) -> Self {
|
||||
let title = Label::centered(title, STYLE.title_style());
|
||||
let message = Label::centered(message, STYLE.message_style()).vertically_centered();
|
||||
let footer = ResultFooter::new(
|
||||
Label::centered(footer, STYLE.title_style()).vertically_centered(),
|
||||
STYLE,
|
||||
);
|
||||
let header = Label::left_aligned("Failure".into(), TEXT_SMALL_RED).vertically_centered();
|
||||
let title = Label::left_aligned(title, TEXT_NORMAL);
|
||||
let message = Label::left_aligned(message, TEXT_SMALL);
|
||||
let footer = Label::centered(footer, TEXT_SMALL_GREY).vertically_centered();
|
||||
|
||||
Self {
|
||||
bg: Pad::with_background(FATAL_ERROR_COLOR).with_clear(),
|
||||
header,
|
||||
title,
|
||||
message,
|
||||
footer,
|
||||
screen_border: ScreenBorder::new(RED),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,24 +47,23 @@ impl<'a> Component for ErrorScreen<'a> {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, _bounds: Rect) -> Rect {
|
||||
self.bg.place(screen());
|
||||
let area = SCREEN.inset(SIDE_INSETS);
|
||||
|
||||
let title_area = Rect::new(
|
||||
Point::new(RESULT_PADDING, TITLE_AREA_START),
|
||||
Point::new(WIDTH - RESULT_PADDING, MESSAGE_AREA_START),
|
||||
);
|
||||
let (header_area, area) = area.split_top(HEADER_HEIGHT);
|
||||
let (area, footer_area) = area.split_bottom(ACTION_BAR_HEIGHT);
|
||||
|
||||
let title_height = self.title.text_height(area.width());
|
||||
let message_height = self.message.text_height(area.width());
|
||||
let (title_area, area) = area.split_top(title_height);
|
||||
let (message_area, _) = area
|
||||
.inset(Insets::top(TEXT_VERTICAL_SPACING))
|
||||
.split_top(message_height);
|
||||
|
||||
self.header.place(header_area);
|
||||
self.title.place(title_area);
|
||||
|
||||
let message_area = Rect::new(
|
||||
Point::new(RESULT_PADDING, MESSAGE_AREA_START),
|
||||
Point::new(WIDTH - RESULT_PADDING, RESULT_FOOTER_START),
|
||||
);
|
||||
self.message.place(message_area);
|
||||
|
||||
let (_, bottom_area) = ResultFooter::<'a>::split_bounds();
|
||||
self.footer.place(bottom_area);
|
||||
|
||||
screen()
|
||||
self.footer.place(footer_area);
|
||||
SCREEN
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
@ -80,17 +71,10 @@ impl<'a> Component for ErrorScreen<'a> {
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
self.bg.render(target);
|
||||
|
||||
let icon = ICON_WARNING40;
|
||||
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
|
||||
.with_fg(WHITE)
|
||||
.with_bg(FATAL_ERROR_COLOR)
|
||||
.with_align(Alignment2D::TOP_CENTER)
|
||||
.render(target);
|
||||
|
||||
self.header.render(target);
|
||||
self.title.render(target);
|
||||
self.message.render(target);
|
||||
self.footer.render(target);
|
||||
self.screen_border.render(u8::MAX, target);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,18 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
geometry::{Alignment, Alignment2D, Offset, Rect},
|
||||
geometry::{Alignment, Offset, Rect},
|
||||
shape,
|
||||
shape::Renderer,
|
||||
};
|
||||
|
||||
use super::super::{fonts, theme};
|
||||
use super::super::{
|
||||
fonts,
|
||||
theme::{GREY_LIGHT, TEXT_VERTICAL_SPACING},
|
||||
};
|
||||
|
||||
const TEXT_BOTTOM_MARGIN: i16 = 54;
|
||||
const ICON_TOP_MARGIN: i16 = 48;
|
||||
|
||||
use crate::trezorhal::model;
|
||||
const TEXT_OFFSET: Offset = Offset::new(30, 40);
|
||||
|
||||
/// Firmware welcome screen
|
||||
pub struct WelcomeScreen {
|
||||
area: Rect,
|
||||
}
|
||||
@ -35,26 +36,26 @@ impl Component for WelcomeScreen {
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
shape::ToifImage::new(
|
||||
self.area.top_center() + Offset::y(ICON_TOP_MARGIN),
|
||||
theme::ICON_LOGO.toif,
|
||||
)
|
||||
.with_align(Alignment2D::TOP_CENTER)
|
||||
.with_fg(theme::FG)
|
||||
.with_bg(theme::BG)
|
||||
.render(target);
|
||||
// TODO: should we use `model::FULL_NAME`?
|
||||
|
||||
shape::Text::new(
|
||||
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
|
||||
model::FULL_NAME,
|
||||
fonts::FONT_SATOSHI_REGULAR_38,
|
||||
)
|
||||
.with_align(Alignment::Center)
|
||||
.with_fg(theme::FG)
|
||||
.render(target);
|
||||
const NAME_PARTS: [&str; 3] = ["Trezor", "Safe", "5"];
|
||||
|
||||
let font = fonts::FONT_SATOSHI_REGULAR_38;
|
||||
let mut cursor = self.area.top_left() + TEXT_OFFSET;
|
||||
let row_height = font.text_height() + TEXT_VERTICAL_SPACING;
|
||||
|
||||
for part in &NAME_PARTS {
|
||||
shape::Text::new(cursor, part, font)
|
||||
.with_align(Alignment::Start)
|
||||
.with_fg(GREY_LIGHT)
|
||||
.render(target);
|
||||
cursor = cursor + Offset::y(row_height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "bootloader"))]
|
||||
use crate::trezorhal::model;
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for WelcomeScreen {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
|
@ -1,40 +1,96 @@
|
||||
use crate::ui::{display::Color, geometry::Point, shape, shape::Renderer};
|
||||
use crate::ui::{
|
||||
constant::SCREEN,
|
||||
geometry::{Offset, Rect},
|
||||
lerp::Lerp,
|
||||
shape::{self, Renderer},
|
||||
};
|
||||
|
||||
use super::super::constant;
|
||||
|
||||
pub enum LoaderRange {
|
||||
Full,
|
||||
FromTo(f32, f32),
|
||||
}
|
||||
use super::{super::theme::BG, ScreenBorder};
|
||||
|
||||
pub fn render_loader<'s>(
|
||||
center: Point,
|
||||
inactive_color: Color,
|
||||
active_color: Color,
|
||||
background_color: Color,
|
||||
range: LoaderRange,
|
||||
progress: u16,
|
||||
border: &'static ScreenBorder,
|
||||
target: &mut impl Renderer<'s>,
|
||||
) {
|
||||
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||
.with_bg(inactive_color)
|
||||
.render(target);
|
||||
// convert to ration from 0.0 to 1.0
|
||||
let progress_ratio = (progress as f32 / 1000.0).clamp(0.0, 1.0);
|
||||
let (clip, top_gap) = get_clips(progress_ratio);
|
||||
render_clipped_border(border, clip, top_gap, u8::MAX, target);
|
||||
}
|
||||
|
||||
match range {
|
||||
LoaderRange::Full => {
|
||||
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||
.with_bg(active_color)
|
||||
.render(target);
|
||||
fn get_clips(progress_ratio: f32) -> (Rect, Rect) {
|
||||
/// Ratio of total_duration for the bottom part of the border
|
||||
const BOTTOM_DURATION_RATIO: f32 = 0.125;
|
||||
/// Ratio of total_duration for the side parts of the border
|
||||
const SIDES_DURATION_RATIO: f32 = 0.5;
|
||||
/// Ratio of total_duration for the top part of the border
|
||||
const TOP_DURATION_RATIO: f32 = 0.375;
|
||||
|
||||
const TOP_GAP_ZERO: Rect = Rect::from_center_and_size(
|
||||
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
|
||||
Offset::zero(),
|
||||
);
|
||||
const TOP_GAP_FULL: Rect = Rect::from_center_and_size(
|
||||
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
|
||||
Offset::new(SCREEN.width(), ScreenBorder::WIDTH),
|
||||
);
|
||||
|
||||
match progress_ratio {
|
||||
// Bottom phase growing linearly
|
||||
p if p < BOTTOM_DURATION_RATIO => {
|
||||
let bottom_progress = (p / BOTTOM_DURATION_RATIO).clamp(0.0, 1.0);
|
||||
let width = i16::lerp(0, SCREEN.width(), bottom_progress);
|
||||
let clip = Rect::from_center_and_size(
|
||||
SCREEN
|
||||
.bottom_center()
|
||||
.ofs(Offset::y(-ScreenBorder::WIDTH / 2)),
|
||||
Offset::new(width, ScreenBorder::WIDTH),
|
||||
);
|
||||
(clip, TOP_GAP_FULL)
|
||||
}
|
||||
LoaderRange::FromTo(start, end) => {
|
||||
shape::Circle::new(center, constant::LOADER_OUTER)
|
||||
.with_bg(active_color)
|
||||
.with_start_angle(start)
|
||||
.with_end_angle(end)
|
||||
.render(target);
|
||||
|
||||
// Sides phase growing up linearly
|
||||
p if p < (BOTTOM_DURATION_RATIO + SIDES_DURATION_RATIO) => {
|
||||
let sides_progress =
|
||||
((p - BOTTOM_DURATION_RATIO) / SIDES_DURATION_RATIO).clamp(0.0, 1.0);
|
||||
let height = i16::lerp(ScreenBorder::WIDTH, SCREEN.height(), sides_progress);
|
||||
let clip = Rect::from_bottom_left_and_size(
|
||||
SCREEN.bottom_left(),
|
||||
Offset::new(SCREEN.width(), height),
|
||||
);
|
||||
(clip, TOP_GAP_FULL)
|
||||
}
|
||||
|
||||
// Top gap shrinking linearly
|
||||
p if p < 1.0 => {
|
||||
let top_progress = ((p - BOTTOM_DURATION_RATIO - SIDES_DURATION_RATIO)
|
||||
/ TOP_DURATION_RATIO)
|
||||
.clamp(0.0, 1.0);
|
||||
let width = i16::lerp(SCREEN.width(), 0, top_progress);
|
||||
let top_gap = Rect::from_center_and_size(
|
||||
SCREEN.top_center().ofs(Offset::y(ScreenBorder::WIDTH / 2)),
|
||||
Offset::new(width, ScreenBorder::WIDTH),
|
||||
);
|
||||
(SCREEN, top_gap)
|
||||
}
|
||||
|
||||
// Animation complete
|
||||
_ => (SCREEN, TOP_GAP_ZERO),
|
||||
}
|
||||
}
|
||||
|
||||
shape::Circle::new(center, constant::LOADER_INNER + 2)
|
||||
.with_bg(background_color)
|
||||
fn render_clipped_border<'s>(
|
||||
border: &'static ScreenBorder,
|
||||
clip: Rect,
|
||||
top_gap: Rect,
|
||||
alpha: u8,
|
||||
target: &mut impl Renderer<'s>,
|
||||
) {
|
||||
target.in_clip(clip, &|target| {
|
||||
border.render(alpha, target);
|
||||
});
|
||||
shape::Bar::new(top_gap)
|
||||
.with_bg(BG)
|
||||
.with_fg(BG)
|
||||
.render(target);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
mod loader;
|
||||
mod screen_border;
|
||||
|
||||
pub use loader::{render_loader, LoaderRange};
|
||||
pub use loader::render_loader;
|
||||
pub use screen_border::ScreenBorder;
|
||||
|
@ -17,7 +17,7 @@ pub struct ScreenBorder {
|
||||
|
||||
impl ScreenBorder {
|
||||
pub const WIDTH: i16 = 4;
|
||||
pub fn new(color: Color) -> Self {
|
||||
pub const fn new(color: Color) -> Self {
|
||||
let screen = constant::screen();
|
||||
|
||||
// Top bar: from the right edge of top-left icon to the left edge of top-right
|
||||
|
Loading…
Reference in New Issue
Block a user