1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-04-07 10:55:56 +00:00

feat(eckhart): implement homescreen and lockscreen

- HomeScreen, ConfirmHomescreen full-screen components
- respective FirmwareUI implementation
This commit is contained in:
obrusvit 2025-03-13 23:46:13 +01:00
parent 2a825c1634
commit 8f2ee16557
12 changed files with 483 additions and 49 deletions

View File

@ -2,19 +2,22 @@
//! current feature (Trezor model)
#[cfg(all(
feature = "layout_bolt",
feature = "layout_eckhart",
not(feature = "layout_bolt"),
not(feature = "layout_caesar"),
not(feature = "layout_delizia"),
not(feature = "layout_eckhart")
not(feature = "layout_delizia")
))]
pub use super::layout_bolt::constant::*;
#[cfg(all(
feature = "layout_caesar",
not(feature = "layout_delizia"),
not(feature = "layout_eckhart")
))]
pub use super::layout_caesar::constant::*;
#[cfg(all(feature = "layout_delizia", not(feature = "layout_eckhart")))]
pub use super::layout_delizia::constant::*;
#[cfg(feature = "layout_eckhart")]
pub use super::layout_eckhart::constant::*;
#[cfg(all(
feature = "layout_delizia",
not(feature = "layout_bolt"),
not(feature = "layout_caesar")
))]
pub use super::layout_delizia::constant::*;
#[cfg(all(feature = "layout_caesar", not(feature = "layout_bolt")))]
pub use super::layout_caesar::constant::*;
#[cfg(feature = "layout_bolt")]
pub use super::layout_bolt::constant::*;

View File

@ -14,9 +14,10 @@ use crate::{
};
use super::firmware::{
AllowedTextContent, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputScreen,
NumberInputScreenMsg, PinKeyboard, PinKeyboardMsg, SelectWordCountMsg, SelectWordCountScreen,
SelectWordMsg, SelectWordScreen, TextScreen, TextScreenMsg,
AllowedTextContent, ConfirmHomescreen, ConfirmHomescreenMsg, Homescreen, HomescreenMsg,
MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NumberInputScreen, NumberInputScreenMsg,
PinKeyboard, PinKeyboardMsg, SelectWordCountMsg, SelectWordCountScreen, SelectWordMsg,
SelectWordScreen, TextScreen, TextScreenMsg,
};
impl ComponentMsgObj for PinKeyboard<'_> {
@ -69,6 +70,14 @@ where
}
}
impl ComponentMsgObj for Homescreen {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
HomescreenMsg::Dismissed => Ok(CANCELLED.as_obj()),
}
}
}
impl<T> ComponentMsgObj for TextScreen<T>
where
T: AllowedTextContent,
@ -109,3 +118,12 @@ impl ComponentMsgObj for NumberInputScreen {
}
}
}
impl ComponentMsgObj for ConfirmHomescreen {
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
ConfirmHomescreenMsg::Cancelled => Ok(CANCELLED.as_obj()),
ConfirmHomescreenMsg::Confirmed => Ok(CONFIRMED.as_obj()),
}
}
}

View File

@ -5,14 +5,9 @@ use crate::trezorhal::display::{DISPLAY_RESX, DISPLAY_RESY};
pub const WIDTH: i16 = DISPLAY_RESX as _;
pub const HEIGHT: i16 = DISPLAY_RESY as _;
// TODO: below constants copied from mercury
pub const LINE_SPACE: i16 = 4;
pub const FONT_BPP: i16 = 4;
pub const LOADER_OUTER: i16 = 60;
pub const LOADER_INNER: i16 = 52;
pub const LOADER_ICON_MAX_SIZE: i16 = 64;
pub const fn size() -> Offset {
Offset::new(WIDTH, HEIGHT)
}

View File

@ -105,6 +105,13 @@ impl ActionBar {
)
}
pub fn new_cancel_confirm() -> Self {
Self::new_double(
Button::with_icon(theme::ICON_CROSS).styled(theme::button_cancel()),
Button::with_text(TR::buttons__confirm.into()),
)
}
pub fn with_left_short(mut self, left_short: bool) -> Self {
self.left_short = left_short;
self

View File

@ -0,0 +1,108 @@
use crate::{
error::{value_error, Error},
io::BinaryData,
strutil::TString,
translations::TR,
ui::{
component::{Component, Event, EventCtx, Label},
constant::SCREEN,
display::image::ImageInfo,
geometry::{Insets, Rect},
shape::{self, Renderer},
},
};
use super::{check_homescreen_format, theme, ActionBar, ActionBarMsg, Header};
/// Full-screen component for confirming a new homescreen image. If the image is
/// empty, the user is asked to confirm the default homescreen.
pub struct ConfirmHomescreen {
header: Header,
text: Option<Label<'static>>,
image: Option<BinaryData<'static>>,
action_bar: ActionBar,
}
pub enum ConfirmHomescreenMsg {
Cancelled,
Confirmed,
}
impl ConfirmHomescreen {
pub fn new(title: TString<'static>, image: BinaryData<'static>) -> Result<Self, Error> {
let action_bar = ActionBar::new_cancel_confirm();
let header = Header::new(title);
if image.is_empty() {
// Use default homescreen
Ok(Self {
header,
text: Some(Label::left_aligned(
TR::homescreen__set_default.into(),
theme::firmware::TEXT_REGULAR,
)),
image: None,
action_bar,
})
} else {
// Validate and use custom homescreen
if !check_homescreen_format(image) {
return Err(value_error!(c"Invalid image."));
}
Ok(Self {
header,
text: None,
image: Some(image),
action_bar,
})
}
}
}
impl Component for ConfirmHomescreen {
type Msg = ConfirmHomescreenMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// assert full screen
debug_assert_eq!(bounds.height(), SCREEN.height());
debug_assert_eq!(bounds.width(), SCREEN.width());
let (header_area, rest) = bounds.split_top(Header::HEADER_HEIGHT);
let (rest, action_bar_area) = rest.split_bottom(ActionBar::ACTION_BAR_HEIGHT);
self.header.place(header_area);
self.action_bar.place(action_bar_area);
self.text.place(rest);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.action_bar.event(ctx, event).and_then(|msg| match msg {
ActionBarMsg::Cancelled => Some(Self::Msg::Cancelled),
ActionBarMsg::Confirmed => Some(Self::Msg::Confirmed),
_ => None,
})
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if let Some(image) = self.image {
if let ImageInfo::Jpeg(_) = ImageInfo::parse(image) {
let clip = SCREEN.inset(Insets::bottom(theme::ACTION_BAR_HEIGHT));
target.in_clip(clip, &|t| {
shape::JpegImage::new_image(SCREEN.top_left(), image).render(t);
});
}
}
self.header.render(target);
self.text.render(target);
self.action_bar.render(target);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for ConfirmHomescreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("ConfirmHomescreen");
}
}

View File

@ -62,6 +62,7 @@ impl<'a> Hint<'a> {
Instruction::new(text.into(), theme::GREY, icon, Some(theme::GREY_LIGHT));
Self::from_content(HintContent::Instruction(instruction_component))
}
pub fn new_instruction_green<T: Into<TString<'static>>>(text: T, icon: Option<Icon>) -> Self {
let instruction_component = Instruction::new(
text.into(),
@ -72,6 +73,16 @@ impl<'a> Hint<'a> {
Self::from_content(HintContent::Instruction(instruction_component))
}
pub fn new_warning_severe<T: Into<TString<'static>>>(text: T) -> Self {
let instruction_component = Instruction::new(
text.into(),
theme::RED,
Some(theme::ICON_WARNING),
Some(theme::RED),
);
Self::from_content(HintContent::Instruction(instruction_component))
}
pub fn new_page_counter() -> Self {
Self::from_content(HintContent::PageCounter(PageCounter::new()))
}

View File

@ -0,0 +1,236 @@
use crate::{
error::Error,
io::BinaryData,
strutil::TString,
translations::TR,
ui::{
component::{text::TextStyle, Component, Event, EventCtx, Label, Never},
display::image::ImageInfo,
geometry::{Insets, Offset, Rect},
layout::util::get_user_custom_image,
shape::{self, Renderer},
},
};
use super::{
super::{component::Button, fonts},
constant::{HEIGHT, SCREEN, WIDTH},
theme::{self, firmware::button_homebar_style, BLACK, GREEN_DARK, GREEN_EXTRA_DARK},
ActionBar, ActionBarMsg, Hint,
};
/// Full-screen component for the homescreen and lockscreen.
pub struct Homescreen {
/// Device name with shadow
label: HomeLabel,
/// Notification
hint: Option<Hint<'static>>,
/// Home action bar
action_bar: ActionBar,
/// Background image
image: Option<BinaryData<'static>>,
/// Whether the PIN is set and device can be locked
lockable: bool,
/// Whether the homescreen is locked
locked: bool,
}
pub enum HomescreenMsg {
Dismissed,
}
impl Homescreen {
pub fn new(
label: TString<'static>,
lockable: bool,
locked: bool,
bootscreen: bool,
coinjoin_authorized: bool,
notification: Option<(TString<'static>, u8)>,
) -> Result<Self, Error> {
let image = get_homescreen_image();
// Notification
let mut notification_level = 4;
let hint = if let Some((text, level)) = notification {
notification_level = level;
if notification_level == 0 {
Some(Hint::new_warning_severe(text))
} else {
Some(Hint::new_instruction(text, Some(theme::ICON_INFO)))
}
} else if locked && coinjoin_authorized {
Some(Hint::new_instruction_green(
TR::coinjoin__do_not_disconnect,
Some(theme::ICON_INFO),
))
} else {
None
};
// ActionBar button
let button_style = button_homebar_style(notification_level);
let button = if bootscreen {
Button::with_homebar_content(Some(TR::lockscreen__tap_to_connect.into()))
.styled(button_style)
} else if locked {
Button::with_homebar_content(Some(TR::lockscreen__tap_to_unlock.into()))
.styled(button_style)
} else {
// TODO: Battery/Connectivity button content
Button::with_homebar_content(None).styled(button_style)
};
Ok(Self {
label: HomeLabel::new(label),
hint,
action_bar: ActionBar::new_single(button),
image,
lockable,
locked,
})
}
}
impl Component for Homescreen {
type Msg = HomescreenMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// assert full screen
debug_assert_eq!(bounds.height(), SCREEN.height());
debug_assert_eq!(bounds.width(), SCREEN.width());
let (rest, bar_area) = bounds.split_bottom(theme::ACTION_BAR_HEIGHT);
let rest = if let Some(hint) = &mut self.hint {
let (rest, hint_area) = rest.split_bottom(hint.height());
hint.place(hint_area);
rest
} else {
rest
};
let label_area = rest
.inset(theme::SIDE_INSETS)
.inset(Insets::top(theme::PADDING));
self.label.place(label_area);
self.action_bar.place(bar_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(ActionBarMsg::Confirmed) = self.action_bar.event(ctx, event) {
if self.locked {
return Some(HomescreenMsg::Dismissed);
} else {
// TODO: Show menu and handle "lock" action differently
if self.lockable {
return Some(HomescreenMsg::Dismissed);
}
}
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
if let Some(image) = self.image {
if let ImageInfo::Jpeg(_) = ImageInfo::parse(image) {
shape::JpegImage::new_image(SCREEN.top_left(), image).render(target);
}
} else {
render_default_hs(target);
}
self.label.render(target);
self.hint.render(target);
self.action_bar.render(target);
}
}
/// Helper component to render a label with a shadow.
struct HomeLabel {
label: Label<'static>,
label_shadow: Label<'static>,
}
impl HomeLabel {
const LABEL_SHADOW_OFFSET: Offset = Offset::uniform(2);
const LABEL_TEXT_STYLE: TextStyle = theme::firmware::TEXT_BIG;
const LABEL_SHADOW_TEXT_STYLE: TextStyle = TextStyle::new(
fonts::FONT_SATOSHI_EXTRALIGHT_46,
BLACK,
BLACK,
BLACK,
BLACK,
);
fn new(label: TString<'static>) -> Self {
let label_primary = Label::left_aligned(label, Self::LABEL_TEXT_STYLE).top_aligned();
let label_shadow = Label::left_aligned(label, Self::LABEL_SHADOW_TEXT_STYLE).top_aligned();
Self {
label: label_primary,
label_shadow,
}
}
fn inner(&self) -> &Label<'static> {
&self.label
}
}
impl Component for HomeLabel {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.label.place(bounds);
self.label_shadow
.place(bounds.translate(Self::LABEL_SHADOW_OFFSET));
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.label_shadow.render(target);
self.label.render(target);
}
}
pub fn check_homescreen_format(image: BinaryData) -> bool {
match ImageInfo::parse(image) {
ImageInfo::Jpeg(info) => {
info.width() == WIDTH && info.height() == HEIGHT && info.mcu_height() <= 16
}
_ => false,
}
}
fn render_default_hs<'a>(target: &mut impl Renderer<'a>) {
shape::Bar::new(SCREEN)
.with_fg(theme::BG)
.with_bg(theme::BG)
.render(target);
shape::Circle::new(SCREEN.center(), 48)
.with_fg(GREEN_DARK)
.with_thickness(4)
.render(target);
shape::Circle::new(SCREEN.center(), 42)
.with_fg(GREEN_EXTRA_DARK)
.with_thickness(4)
.render(target);
}
fn get_homescreen_image() -> Option<BinaryData<'static>> {
if let Ok(image) = get_user_custom_image() {
if check_homescreen_format(image) {
return Some(image);
}
}
None
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Homescreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Homescreen");
t.child("label", self.label.inner());
}
}

View File

@ -1,7 +1,9 @@
mod action_bar;
mod confirm_homescreen;
mod header;
mod hint;
mod hold_to_confirm;
mod homescreen;
mod keyboard;
mod number_input_screen;
mod qr_screen;
@ -12,10 +14,11 @@ mod vertical_menu;
mod vertical_menu_screen;
pub use action_bar::{ActionBar, ActionBarMsg};
pub use confirm_homescreen::{ConfirmHomescreen, ConfirmHomescreenMsg};
pub use header::{Header, HeaderMsg};
pub use hint::Hint;
pub use hold_to_confirm::HoldToConfirmAnim;
#[cfg(feature = "translations")]
pub use homescreen::{check_homescreen_format, Homescreen, HomescreenMsg};
pub use keyboard::{
bip39::Bip39Input,
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},
@ -27,7 +30,6 @@ pub use keyboard::{
pub use number_input_screen::{NumberInputScreen, NumberInputScreenMsg};
pub use qr_screen::{QrMsg, QrScreen};
pub use select_word_screen::{SelectWordMsg, SelectWordScreen};
#[cfg(feature = "translations")]
pub use share_words::{ShareWordsScreen, ShareWordsScreenMsg};
pub use text_screen::{AllowedTextContent, TextScreen, TextScreenMsg};
pub use vertical_menu::{VerticalMenu, VerticalMenuMsg, MENU_MAX_ITEMS};

View File

@ -8,8 +8,7 @@ use super::{
component::{ButtonStyle, ButtonStyleSheet},
fonts,
},
BLACK, BLUE, GREY, GREY_DARK, GREY_EXTRA_DARK, GREY_EXTRA_LIGHT, GREY_LIGHT, GREY_SUPER_DARK,
RED, WHITE,
BLACK, BLUE, GREY, GREY_DARK, GREY_EXTRA_LIGHT, GREY_LIGHT, GREY_SUPER_DARK, RED, WHITE,
};
pub const BLD_BG: Color = BLACK;

View File

@ -287,6 +287,44 @@ pub const fn menu_item_title_orange() -> ButtonStyleSheet {
menu_item_title!(ORANGE)
}
macro_rules! button_homebar_style {
($text_color:expr, $icon_color:expr) => {
ButtonStyleSheet {
normal: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: $text_color,
button_color: BG,
icon_color: $icon_color,
background_color: BG,
},
active: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: GREY_SUPER_DARK,
icon_color: GREY_LIGHT,
background_color: GREY_SUPER_DARK,
},
disabled: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: GREY_SUPER_DARK,
icon_color: GREY_LIGHT,
background_color: GREY_SUPER_DARK,
},
}
};
}
pub const fn button_homebar_style(level: u8) -> ButtonStyleSheet {
// NOTE: 0 is the highest severity.
match level {
4 => button_homebar_style!(GREY_LIGHT, GREY_LIGHT),
3 => button_homebar_style!(GREY_LIGHT, GREEN_LIME),
2 => button_homebar_style!(GREY_LIGHT, YELLOW),
1 => button_homebar_style!(GREY_LIGHT, YELLOW),
_ => button_homebar_style!(RED, RED),
}
}
pub const fn button_select_word() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {

View File

@ -45,8 +45,9 @@ pub const FATAL_ERROR_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
// Common constants
pub const PADDING: i16 = 24; // px
pub const HEADER_HEIGHT: i16 = 96; // [px]
pub const SIDE_INSETS: Insets = Insets::sides(24); // [px]
pub const SIDE_INSETS: Insets = Insets::sides(PADDING);
pub const ACTION_BAR_HEIGHT: i16 = 90; // [px]
pub const TEXT_VERTICAL_SPACING: i16 = 24; // [px]

View File

@ -27,8 +27,9 @@ use crate::{
use super::{
component::Button,
firmware::{
ActionBar, Bip39Input, Header, HeaderMsg, Hint, MnemonicKeyboard, NumberInputScreen,
PinKeyboard, SelectWordCountScreen, SelectWordScreen, Slip39Input, TextScreen,
ActionBar, Bip39Input, ConfirmHomescreen, Header, HeaderMsg, Hint, Homescreen,
MnemonicKeyboard, NumberInputScreen, PinKeyboard, SelectWordCountScreen, SelectWordScreen,
Slip39Input, TextScreen,
},
flow, fonts, theme, UIEckhart,
};
@ -105,10 +106,12 @@ impl FirmwareUI for UIEckhart {
}
fn confirm_homescreen(
_title: TString<'static>,
_image: BinaryData<'static>,
title: TString<'static>,
image: BinaryData<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
let screen = ConfirmHomescreen::new(title, image)?;
let layout = RootComponent::new(screen);
Ok(layout)
}
fn confirm_coinjoin(
@ -314,8 +317,8 @@ impl FirmwareUI for UIEckhart {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn check_homescreen_format(_image: BinaryData, _accept_toif: bool) -> bool {
false // not implemented
fn check_homescreen_format(image: BinaryData, _accept_toif: bool) -> bool {
super::firmware::check_homescreen_format(image)
}
fn continue_recovery_homepage(
@ -570,20 +573,22 @@ impl FirmwareUI for UIEckhart {
fn show_homescreen(
label: TString<'static>,
_hold: bool,
hold: bool,
notification: Option<TString<'static>>,
_notification_level: u8,
notification_level: u8,
) -> Result<impl LayoutMaybeTrace, Error> {
let paragraphs = ParagraphVecShort::from_iter([
Paragraph::new(&theme::TEXT_NORMAL, label),
Paragraph::new(
&theme::TEXT_NORMAL,
notification.unwrap_or(TString::empty()),
),
])
.into_paragraphs();
let layout = RootComponent::new(paragraphs);
let locked = false;
let bootscreen = false;
let coinjoin_authorized = false;
let notification = notification.map(|w| (w, notification_level));
let layout = RootComponent::new(Homescreen::new(
label,
hold,
locked,
bootscreen,
coinjoin_authorized,
notification,
)?);
Ok(layout)
}
@ -632,11 +637,22 @@ impl FirmwareUI for UIEckhart {
}
fn show_lockscreen(
_label: TString<'static>,
_bootscreen: bool,
_coinjoin_authorized: bool,
label: TString<'static>,
bootscreen: bool,
coinjoin_authorized: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
let locked = true;
let notification = None;
let hold = false;
let layout = RootComponent::new(Homescreen::new(
label,
hold,
locked,
bootscreen,
coinjoin_authorized,
notification,
)?);
Ok(layout)
}
fn show_mismatch(title: TString<'static>) -> Result<impl LayoutMaybeTrace, Error> {