1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-28 15:22:14 +00:00

feat(eckhart): initial commit

- build
- directory structure
- bootloader UI and assets copied from delizia
- FirmwareUI trait functions are empty
- Python layout functions are copied from delizia except some of more
complicated ones which raise NotImplemented for now
This commit is contained in:
obrusvit 2024-12-19 16:38:30 +01:00
parent f5a51b0671
commit ffc0bf8e45
61 changed files with 4624 additions and 15 deletions

View File

@ -1,3 +1,3 @@
MCU = STM32U5 MCU = STM32U5
OPENOCD_TARGET = target/stm32u5x.cfg OPENOCD_TARGET = target/stm32u5x.cfg
LAYOUT_FEATURE = layout_bolt LAYOUT_FEATURE = layout_eckhart

View File

@ -11,6 +11,7 @@ crypto = ["zeroize"]
layout_bolt = [] layout_bolt = []
layout_caesar = [] layout_caesar = []
layout_delizia = [] layout_delizia = []
layout_eckhart = []
micropython = [] micropython = []
protobuf = ["micropython"] protobuf = ["micropython"]
ui = [] ui = []

View File

@ -3,11 +3,18 @@
#[cfg(all( #[cfg(all(
feature = "layout_bolt", feature = "layout_bolt",
not(feature = "layout_caesar"),
not(feature = "layout_delizia"), not(feature = "layout_delizia"),
not(feature = "layout_caesar") not(feature = "layout_eckhart")
))] ))]
pub use super::layout_bolt::constant::*; pub use super::layout_bolt::constant::*;
#[cfg(all(feature = "layout_caesar", not(feature = "layout_delizia")))] #[cfg(all(
feature = "layout_caesar",
not(feature = "layout_delizia"),
not(feature = "layout_eckhart")
))]
pub use super::layout_caesar::constant::*; pub use super::layout_caesar::constant::*;
#[cfg(feature = "layout_delizia")] #[cfg(all(feature = "layout_delizia", not(feature = "layout_eckhart")))]
pub use super::layout_delizia::constant::*; pub use super::layout_delizia::constant::*;
#[cfg(feature = "layout_eckhart")]
pub use super::layout_eckhart::constant::*;

View File

@ -30,6 +30,10 @@ const fn clamp(x: i16, min: i16, max: i16) -> i16 {
/// Relative offset in 2D space, used for representing translation and /// Relative offset in 2D space, used for representing translation and
/// dimensions of objects. Absolute positions on the screen are represented by /// dimensions of objects. Absolute positions on the screen are represented by
/// the `Point` type. /// the `Point` type.
///
/// Coordinate system orientation:
/// * x-axis: negative values go left, positive values go right
/// * y-axis: negative values go up, positive values go down
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Offset { pub struct Offset {
pub x: i16, pub x: i16,

View File

@ -0,0 +1,113 @@
use crate::{
strutil::TString,
ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
constant::screen,
display::Icon,
geometry::{Alignment, Insets, Point, Rect},
shape::Renderer,
},
};
use super::super::{
component::{Button, ButtonMsg::Clicked},
constant::WIDTH,
theme::bootloader::{
button_bld, button_bld_menu, text_title, BLD_BG, BUTTON_AREA_START, BUTTON_HEIGHT,
CONTENT_PADDING, CORNER_BUTTON_AREA, MENU32, TEXT_NORMAL, TEXT_WARNING, TITLE_AREA,
},
};
#[repr(u32)]
#[derive(Copy, Clone, ToPrimitive)]
pub enum IntroMsg {
Menu = 1,
Host = 2,
}
pub struct Intro<'a> {
bg: Pad,
title: Child<Label<'a>>,
menu: Child<Button>,
host: Child<Button>,
text: Child<Label<'a>>,
warn: Option<Child<Label<'a>>>,
}
impl<'a> Intro<'a> {
pub fn new(title: TString<'a>, content: TString<'a>, fw_ok: bool) -> Self {
Self {
bg: Pad::with_background(BLD_BG).with_clear(),
title: Child::new(Label::left_aligned(title, text_title(BLD_BG)).vertically_centered()),
menu: Child::new(
Button::with_icon(Icon::new(MENU32))
.styled(button_bld_menu())
.with_expanded_touch_area(Insets::uniform(13)),
),
host: Child::new(
Button::with_text("INSTALL FIRMWARE".into())
.styled(button_bld())
.with_text_align(Alignment::Center),
),
text: Child::new(Label::left_aligned(content, TEXT_NORMAL).vertically_centered()),
warn: (!fw_ok).then_some(Child::new(
Label::new("FIRMWARE CORRUPTED".into(), Alignment::Start, TEXT_WARNING)
.vertically_centered(),
)),
}
}
}
impl<'a> Component for Intro<'a> {
type Msg = IntroMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(screen());
self.title.place(TITLE_AREA);
self.menu.place(CORNER_BUTTON_AREA);
self.host.place(Rect::new(
Point::new(CONTENT_PADDING, BUTTON_AREA_START),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START + BUTTON_HEIGHT),
));
if self.warn.is_some() {
self.warn.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING),
Point::new(
WIDTH - CONTENT_PADDING,
TITLE_AREA.y1 + CONTENT_PADDING + 30,
),
));
self.text.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING + 30),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START - CONTENT_PADDING),
));
} else {
self.text.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1 + CONTENT_PADDING),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START - CONTENT_PADDING),
));
}
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(Clicked) = self.menu.event(ctx, event) {
return Some(Self::Msg::Menu);
};
if let Some(Clicked) = self.host.event(ctx, event) {
return Some(Self::Msg::Host);
};
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.title.render(target);
self.text.render(target);
self.warn.render(target);
self.host.render(target);
self.menu.render(target);
}
}

View File

@ -0,0 +1,111 @@
use crate::{
trezorhal::secbool::{secbool, sectrue},
ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
constant::{screen, WIDTH},
display::Icon,
geometry::{Insets, Point, Rect},
shape::Renderer,
},
};
use super::super::{
component::{Button, ButtonMsg::Clicked, IconText},
theme::bootloader::{
button_bld, button_bld_menu, text_title, BLD_BG, BUTTON_HEIGHT, CONTENT_PADDING,
CORNER_BUTTON_AREA, CORNER_BUTTON_TOUCH_EXPANSION, FIRE24, REFRESH24, TITLE_AREA, X32,
},
};
const BUTTON_AREA_START: i16 = 56;
const BUTTON_SPACING: i16 = 8;
#[repr(u32)]
#[derive(Copy, Clone, ToPrimitive)]
pub enum MenuMsg {
Close = 0xAABBCCDD,
Reboot = 0x11223344,
FactoryReset = 0x55667788,
}
pub struct Menu {
bg: Pad,
title: Child<Label<'static>>,
close: Child<Button>,
reboot: Child<Button>,
reset: Child<Button>,
}
impl Menu {
pub fn new(firmware_present: secbool) -> Self {
let content_reboot = IconText::new("REBOOT TREZOR", Icon::new(REFRESH24));
let content_reset = IconText::new("FACTORY RESET", Icon::new(FIRE24));
let mut instance = Self {
bg: Pad::with_background(BLD_BG),
title: Child::new(
Label::left_aligned("BOOTLOADER".into(), text_title(BLD_BG)).vertically_centered(),
),
close: Child::new(
Button::with_icon(Icon::new(X32))
.styled(button_bld_menu())
.with_expanded_touch_area(Insets::uniform(CORNER_BUTTON_TOUCH_EXPANSION)),
),
reboot: Child::new(
Button::with_icon_and_text(content_reboot)
.styled(button_bld())
.initially_enabled(sectrue == firmware_present),
),
reset: Child::new(Button::with_icon_and_text(content_reset).styled(button_bld())),
};
instance.bg.clear();
instance
}
}
impl Component for Menu {
type Msg = MenuMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(screen());
self.title.place(TITLE_AREA);
self.close.place(CORNER_BUTTON_AREA);
self.reboot.place(Rect::new(
Point::new(CONTENT_PADDING, BUTTON_AREA_START),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START + BUTTON_HEIGHT),
));
self.reset.place(Rect::new(
Point::new(
CONTENT_PADDING,
BUTTON_AREA_START + BUTTON_HEIGHT + BUTTON_SPACING,
),
Point::new(
WIDTH - CONTENT_PADDING,
BUTTON_AREA_START + 2 * BUTTON_HEIGHT + BUTTON_SPACING,
),
));
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(Clicked) = self.close.event(ctx, event) {
return Some(Self::Msg::Close);
}
if let Some(Clicked) = self.reboot.event(ctx, event) {
return Some(Self::Msg::Reboot);
}
if let Some(Clicked) = self.reset.event(ctx, event) {
return Some(Self::Msg::FactoryReset);
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.title.render(target);
self.close.render(target);
self.reboot.render(target);
self.reset.render(target);
}
}

View File

@ -0,0 +1,477 @@
use crate::{
trezorhal::secbool::secbool,
ui::{
component::{connect::Connect, Label},
display::{self, Color, Icon},
geometry::{Alignment, Offset, Point, Rect},
layout::simplified::{run, show},
},
};
use heapless::String;
use super::{
bootloader::welcome::Welcome,
component::{
bl_confirm::{Confirm, ConfirmTitle},
Button, ResultScreen, WelcomeScreen,
},
cshape::{render_loader, LoaderRange},
fonts,
theme::{
backlight,
bootloader::{
button_bld, button_bld_menu, button_confirm, button_wipe_cancel, button_wipe_confirm,
BLD_BG, BLD_FG, BLD_TITLE_COLOR, BLD_WIPE_COLOR, CHECK24, CHECK40, DOWNLOAD24, FIRE32,
FIRE40, RESULT_FW_INSTALL, RESULT_WIPE, TEXT_BOLD, TEXT_NORMAL, TEXT_WIPE_BOLD,
TEXT_WIPE_NORMAL, WARNING40, WELCOME_COLOR, X24,
},
GREEN_LIGHT, GREY,
},
UIEckhart,
};
use crate::ui::{ui_bootloader::BootloaderUI, CommonUI};
use crate::ui::{
display::{toif::Toif, LOADER_MAX},
geometry::Alignment2D,
shape,
shape::render_on_display,
};
use ufmt::uwrite;
use super::theme::bootloader::BLD_WARN_COLOR;
use intro::Intro;
use menu::Menu;
pub mod intro;
pub mod menu;
pub mod welcome;
pub type BootloaderString = String<128>;
const RECONNECT_MESSAGE: &str = "PLEASE RECONNECT\nTHE DEVICE";
const SCREEN: Rect = UIEckhart::SCREEN;
const PROGRESS_TEXT_ORIGIN: Point = Point::new(2, 28);
impl UIEckhart {
fn screen_progress(
text: &str,
progress: u16,
initialize: bool,
fg_color: Color,
bg_color: Color,
icon: Option<(Icon, Color)>,
center_text: Option<&str>,
) {
if initialize {
Self::fadeout();
}
display::sync();
render_on_display(None, Some(bg_color), |target| {
shape::Text::new(PROGRESS_TEXT_ORIGIN, text, fonts::FONT_SATOSHI_REGULAR_38)
.with_fg(BLD_FG)
.render(target);
let loader_offset: i16 = 19;
let center_text_offset: i16 = 10;
let center = SCREEN.center() + Offset::y(loader_offset);
let inactive_color = bg_color.blend(fg_color, 85);
let end = 360.0 * progress as f32 / 1000.0;
render_loader(
center,
inactive_color,
fg_color,
bg_color,
if progress >= LOADER_MAX {
LoaderRange::Full
} else {
LoaderRange::FromTo(0.0, end)
},
target,
);
if let Some((icon, color)) = icon {
shape::ToifImage::new(center, icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(color)
.render(target);
}
if let Some(center_text) = center_text {
shape::Text::new(
SCREEN.center() + Offset::y(loader_offset + center_text_offset),
center_text,
fonts::FONT_SATOSHI_REGULAR_38,
)
.with_align(Alignment::Center)
.with_fg(GREY)
.render(target);
}
});
display::refresh();
if initialize {
Self::fadein();
}
}
}
impl BootloaderUI for UIEckhart {
fn screen_welcome() {
let mut frame = Welcome::new();
show(&mut frame, true);
}
fn screen_install_success(restart_seconds: u8, initial_setup: bool, complete_draw: bool) {
let mut reboot_msg = BootloaderString::new();
let bg_color = if initial_setup { WELCOME_COLOR } else { BLD_BG };
let fg_color = if initial_setup { GREEN_LIGHT } else { BLD_FG };
if restart_seconds >= 1 {
// in practice, restart_seconds is 5 or less so this is fine
let seconds_char = b'0' + restart_seconds % 10;
unwrap!(reboot_msg.push(seconds_char as char));
let progress = (5 - (restart_seconds as u16)).clamp(0, 5) * 200;
Self::screen_progress(
"Restarting device",
progress,
complete_draw,
fg_color,
bg_color,
None,
Some(reboot_msg.as_str()),
);
} else {
Self::screen_progress(
"Firmware installed",
1000,
complete_draw,
fg_color,
bg_color,
Some((Icon::new(CHECK24), BLD_FG)),
None,
);
}
}
fn screen_install_fail() {
let mut frame = ResultScreen::new(
&RESULT_FW_INSTALL,
Icon::new(WARNING40),
"Firmware installation was not successful".into(),
Label::centered(RECONNECT_MESSAGE.into(), RESULT_FW_INSTALL.title_style())
.vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_install_confirm(
vendor: &str,
version: &str,
fingerprint: &str,
should_keep_seed: bool,
is_newvendor: bool,
is_newinstall: bool,
version_cmp: i32,
) -> u32 {
let mut version_str: BootloaderString = String::new();
unwrap!(version_str.push_str("Firmware version "));
unwrap!(version_str.push_str(version));
unwrap!(version_str.push_str("\nby "));
unwrap!(version_str.push_str(vendor));
let title_str = if is_newinstall {
"INSTALL FIRMWARE"
} else if is_newvendor {
"CHANGE FW\nVENDOR"
} else if version_cmp > 0 {
"UPDATE FIRMWARE"
} else if version_cmp == 0 {
"REINSTALL FW"
} else {
"DOWNGRADE FW"
};
let title = Label::left_aligned(title_str.into(), TEXT_BOLD).vertically_centered();
let msg = Label::left_aligned(version_str.as_str().into(), TEXT_NORMAL);
let alert = (!should_keep_seed).then_some(Label::left_aligned(
"SEED WILL BE ERASED!".into(),
TEXT_BOLD,
));
let (left, right) = if should_keep_seed {
let l = Button::with_text("CANCEL".into())
.styled(button_bld())
.with_text_align(Alignment::Center);
let r = Button::with_text("INSTALL".into())
.styled(button_confirm())
.with_text_align(Alignment::Center);
(l, r)
} else {
let l = Button::with_icon(Icon::new(X24))
.styled(button_bld())
.with_text_align(Alignment::Center);
let r = Button::with_icon(Icon::new(CHECK24))
.styled(button_confirm())
.with_text_align(Alignment::Center);
(l, r)
};
let mut frame = Confirm::new(BLD_BG, left, right, ConfirmTitle::Text(title), msg)
.with_info(
"FW FINGERPRINT".into(),
fingerprint.into(),
button_bld_menu(),
);
if let Some(alert) = alert {
frame = frame.with_alert(alert);
}
run(&mut frame)
}
fn screen_wipe_confirm() -> u32 {
let icon = Icon::new(FIRE40);
let msg = Label::centered(
"Are you sure you want to factory reset the device?".into(),
TEXT_WIPE_NORMAL,
);
let alert = Label::centered("SEED AND FIRMWARE\nWILL BE ERASED!".into(), TEXT_WIPE_BOLD);
let right = Button::with_text("RESET".into())
.styled(button_wipe_confirm())
.with_text_align(Alignment::Center);
let left = Button::with_text("CANCEL".into())
.styled(button_wipe_cancel())
.with_text_align(Alignment::Center);
let mut frame = Confirm::new(BLD_WIPE_COLOR, left, right, ConfirmTitle::Icon(icon), msg)
.with_alert(alert);
run(&mut frame)
}
fn screen_unlock_bootloader_confirm() -> u32 {
let title =
Label::left_aligned("UNLOCK BOOTLOADER".into(), TEXT_BOLD).vertically_centered();
let msg = Label::centered("This action cannot be undone!".into(), TEXT_NORMAL);
let right = Button::with_text("UNLOCK".into())
.styled(button_confirm())
.with_text_align(Alignment::Center);
let left = Button::with_text("CANCEL".into())
.styled(button_bld())
.with_text_align(Alignment::Center);
let mut frame = Confirm::new(BLD_BG, left, right, ConfirmTitle::Text(title), msg);
run(&mut frame)
}
fn screen_unlock_bootloader_success() {
let mut frame = ResultScreen::new(
&RESULT_FW_INSTALL,
Icon::new(CHECK40),
"Bootloader unlocked".into(),
Label::centered(RECONNECT_MESSAGE.into(), RESULT_FW_INSTALL.title_style())
.vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_menu(firmware_present: secbool) -> u32 {
run(&mut Menu::new(firmware_present))
}
fn screen_intro(bld_version: &str, vendor: &str, version: &str, fw_ok: bool) -> u32 {
let mut title_str: BootloaderString = String::new();
unwrap!(title_str.push_str("BOOTLOADER "));
unwrap!(title_str.push_str(bld_version));
let mut version_str: BootloaderString = String::new();
unwrap!(version_str.push_str("Firmware version "));
unwrap!(version_str.push_str(version));
unwrap!(version_str.push_str("\nby "));
unwrap!(version_str.push_str(vendor));
let mut frame = Intro::new(
title_str.as_str().into(),
version_str.as_str().into(),
fw_ok,
);
run(&mut frame)
}
fn screen_boot_stage_1(fading: bool) {
if fading {
Self::fadeout();
}
let mut frame = WelcomeScreen::new();
show(&mut frame, false);
if fading {
Self::fadein();
} else {
display::set_backlight(backlight::get_backlight_normal());
}
}
fn screen_wipe_progress(progress: u16, initialize: bool) {
Self::screen_progress(
"Resetting Trezor",
progress,
initialize,
BLD_FG,
BLD_WIPE_COLOR,
Some((Icon::new(FIRE32), BLD_FG)),
None,
)
}
fn screen_install_progress(progress: u16, initialize: bool, initial_setup: bool) {
let bg_color = if initial_setup { WELCOME_COLOR } else { BLD_BG };
let fg_color = if initial_setup { GREEN_LIGHT } else { BLD_FG };
let icon_color = BLD_FG;
Self::screen_progress(
"Installing firmware",
progress,
initialize,
fg_color,
bg_color,
Some((Icon::new(DOWNLOAD24), icon_color)),
None,
)
}
fn screen_connect(initial_setup: bool) {
let bg = if initial_setup { WELCOME_COLOR } else { BLD_BG };
let mut frame = Connect::new(
"Waiting for host...",
fonts::FONT_SATOSHI_REGULAR_38,
BLD_TITLE_COLOR,
bg,
);
show(&mut frame, true);
}
fn screen_wipe_success() {
let mut frame = ResultScreen::new(
&RESULT_WIPE,
Icon::new(CHECK40),
"Trezor reset\nsuccessfully".into(),
Label::centered(RECONNECT_MESSAGE.into(), RESULT_WIPE.title_style())
.vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_wipe_fail() {
let mut frame = ResultScreen::new(
&RESULT_WIPE,
Icon::new(WARNING40),
"Trezor reset was\nnot successful".into(),
Label::centered(RECONNECT_MESSAGE.into(), RESULT_WIPE.title_style())
.vertically_centered(),
true,
);
show(&mut frame, true);
}
fn screen_boot(
warning: bool,
vendor_str: Option<&str>,
version: [u8; 4],
vendor_img: &'static [u8],
wait: i32,
) {
let bg_color = if warning {
BLD_WARN_COLOR
} else {
WELCOME_COLOR
};
display::sync();
render_on_display(None, Some(bg_color), |target| {
// Draw vendor image if it's valid and has size of 120x120
if let Ok(toif) = Toif::new(vendor_img) {
if (toif.width() == 120) && (toif.height() == 120) {
// Image position depends on the vendor string presence
let pos = if vendor_str.is_some() {
Point::new(SCREEN.width() / 2, 30)
} else {
Point::new(SCREEN.width() / 2, 60)
};
shape::ToifImage::new(pos, toif)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(BLD_FG)
.render(target);
}
}
// Draw vendor string if present
if let Some(text) = vendor_str {
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5 - 50);
shape::Text::new(pos, text, fonts::FONT_SATOSHI_REGULAR_38)
.with_align(Alignment::Center)
.with_fg(BLD_FG) //COLOR_BL_BG
.render(target);
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5 - 25);
let mut version_text: BootloaderString = String::new();
unwrap!(uwrite!(
version_text,
"{}.{}.{}",
version[0],
version[1],
version[2]
));
shape::Text::new(pos, version_text.as_str(), fonts::FONT_SATOSHI_REGULAR_38)
.with_align(Alignment::Center)
.with_fg(BLD_FG)
.render(target);
}
// Draw a message
match wait.cmp(&0) {
core::cmp::Ordering::Equal => {}
core::cmp::Ordering::Greater => {
let mut text: BootloaderString = String::new();
unwrap!(uwrite!(text, "starting in {} s", wait));
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5);
shape::Text::new(pos, text.as_str(), fonts::FONT_SATOSHI_REGULAR_38)
.with_align(Alignment::Center)
.with_fg(BLD_FG)
.render(target);
}
core::cmp::Ordering::Less => {
let pos = Point::new(SCREEN.width() / 2, SCREEN.height() - 5);
shape::Text::new(pos, "click to continue ...", fonts::FONT_SATOSHI_REGULAR_38)
.with_align(Alignment::Center)
.with_fg(BLD_FG)
.render(target);
}
}
});
display::refresh();
}
}

View File

@ -0,0 +1,67 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
constant::screen,
display::Font,
geometry::{Offset, Point, Rect},
shape,
shape::Renderer,
};
use super::super::theme::{BLACK, GREY, WHITE};
const TEXT_ORIGIN: Point = Point::new(0, 105);
const STRIDE: i16 = 22;
pub struct Welcome {
bg: Pad,
}
impl Welcome {
pub fn new() -> Self {
Self {
bg: Pad::with_background(BLACK).with_clear(),
}
}
}
impl Component for Welcome {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(screen());
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.bg.render(target);
shape::Text::new(TEXT_ORIGIN, "Get started")
.with_font(Font::NORMAL)
.with_fg(GREY)
.render(target);
shape::Text::new(TEXT_ORIGIN + Offset::y(STRIDE), "with your Trezor")
.with_font(Font::NORMAL)
.with_fg(GREY)
.render(target);
shape::Text::new(TEXT_ORIGIN + Offset::y(2 * STRIDE), "at")
.with_font(Font::NORMAL)
.with_fg(GREY)
.render(target);
let at_width = Font::NORMAL.text_width("at ");
shape::Text::new(
TEXT_ORIGIN + Offset::new(at_width, 2 * STRIDE),
"trezor.io/start",
)
.with_font(Font::NORMAL)
.with_fg(WHITE)
.render(target);
}
}

View File

@ -0,0 +1,248 @@
use crate::{
strutil::TString,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Label, Pad},
constant,
constant::screen,
display::{Color, Icon},
geometry::{Alignment2D, Insets, Offset, Point, Rect},
shape,
shape::Renderer,
},
};
use super::{
constant::WIDTH,
theme::{
bootloader::{
text_fingerprint, text_title, BUTTON_AREA_START, BUTTON_HEIGHT, CONTENT_PADDING,
CORNER_BUTTON_AREA, CORNER_BUTTON_TOUCH_EXPANSION, INFO32, TITLE_AREA, X32,
},
WHITE,
},
Button,
ButtonMsg::Clicked,
ButtonStyleSheet,
};
const ICON_TOP: i16 = 17;
const CONTENT_START: i16 = 72;
const CONTENT_AREA: Rect = Rect::new(
Point::new(CONTENT_PADDING, CONTENT_START),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START - CONTENT_PADDING),
);
#[derive(Copy, Clone, ToPrimitive)]
pub enum ConfirmMsg {
Cancel = 1,
Confirm = 2,
}
pub enum ConfirmTitle {
Text(Label<'static>),
Icon(Icon),
}
pub struct ConfirmInfo<'a> {
pub title: Child<Label<'a>>,
pub text: Child<Label<'a>>,
pub info_button: Child<Button>,
pub close_button: Child<Button>,
}
pub struct Confirm<'a> {
bg: Pad,
content_pad: Pad,
bg_color: Color,
title: ConfirmTitle,
message: Child<Label<'a>>,
alert: Option<Child<Label<'static>>>,
left_button: Child<Button>,
right_button: Child<Button>,
info: Option<ConfirmInfo<'a>>,
show_info: bool,
}
impl<'a> Confirm<'a> {
pub fn new(
bg_color: Color,
left_button: Button,
right_button: Button,
title: ConfirmTitle,
message: Label<'a>,
) -> Self {
Self {
bg: Pad::with_background(bg_color).with_clear(),
content_pad: Pad::with_background(bg_color),
bg_color,
title,
message: Child::new(message.vertically_centered()),
left_button: Child::new(left_button),
right_button: Child::new(right_button),
alert: None,
info: None,
show_info: false,
}
}
pub fn with_alert(mut self, alert: Label<'static>) -> Self {
self.alert = Some(Child::new(alert.vertically_centered()));
self
}
pub fn with_info(
mut self,
title: TString<'a>,
text: TString<'a>,
menu_button: ButtonStyleSheet,
) -> Self {
self.info = Some(ConfirmInfo {
title: Child::new(
Label::left_aligned(title, text_title(self.bg_color)).vertically_centered(),
),
text: Child::new(
Label::left_aligned(text, text_fingerprint(self.bg_color)).vertically_centered(),
),
info_button: Child::new(
Button::with_icon(Icon::new(INFO32))
.styled(menu_button)
.with_expanded_touch_area(Insets::uniform(CORNER_BUTTON_TOUCH_EXPANSION)),
),
close_button: Child::new(
Button::with_icon(Icon::new(X32))
.styled(menu_button)
.with_expanded_touch_area(Insets::uniform(CORNER_BUTTON_TOUCH_EXPANSION)),
),
});
self
}
}
impl Component for Confirm<'_> {
type Msg = ConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.bg.place(constant::screen());
self.content_pad.place(Rect::new(
Point::zero(),
Point::new(WIDTH, BUTTON_AREA_START),
));
let mut content_area = CONTENT_AREA;
match &mut self.title {
ConfirmTitle::Icon(_) => {
// XXX HACK: when icon is present (wipe device screen), we know the
// string is long and we need to go outside the content padding
content_area = content_area.inset(Insets::sides(-CONTENT_PADDING));
}
ConfirmTitle::Text(title) => {
title.place(TITLE_AREA);
}
};
if self.alert.is_some() {
let message_height = self.message.inner().text_height(content_area.width());
self.message.place(Rect::from_top_left_and_size(
content_area.top_left(),
Offset::new(content_area.width(), message_height),
));
let (_, alert_bounds) = content_area.split_top(message_height);
self.alert.place(alert_bounds);
} else {
self.message.place(content_area);
}
let button_size = Offset::new((WIDTH - 3 * CONTENT_PADDING) / 2, BUTTON_HEIGHT);
self.left_button.place(Rect::from_top_left_and_size(
Point::new(CONTENT_PADDING, BUTTON_AREA_START),
button_size,
));
self.right_button.place(Rect::from_top_left_and_size(
Point::new(2 * CONTENT_PADDING + button_size.x, BUTTON_AREA_START),
button_size,
));
if let Some(info) = self.info.as_mut() {
info.info_button.place(CORNER_BUTTON_AREA);
info.close_button.place(CORNER_BUTTON_AREA);
info.title.place(TITLE_AREA);
info.text.place(Rect::new(
Point::new(CONTENT_PADDING, TITLE_AREA.y1),
Point::new(WIDTH - CONTENT_PADDING, BUTTON_AREA_START),
));
}
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(info) = self.info.as_mut() {
if self.show_info {
if let Some(Clicked) = info.close_button.event(ctx, event) {
self.show_info = false;
self.content_pad.clear();
self.message.request_complete_repaint(ctx);
self.alert.request_complete_repaint(ctx);
return None;
}
} else if let Some(Clicked) = info.info_button.event(ctx, event) {
self.show_info = true;
info.text.request_complete_repaint(ctx);
info.title.request_complete_repaint(ctx);
self.content_pad.clear();
return None;
}
}
if let Some(Clicked) = self.left_button.event(ctx, event) {
return Some(Self::Msg::Cancel);
};
if let Some(Clicked) = self.right_button.event(ctx, event) {
return Some(Self::Msg::Confirm);
};
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.content_pad.render(target);
if let Some(info) = self.info.as_ref() {
if self.show_info {
info.close_button.render(target);
info.title.render(target);
info.text.render(target);
self.left_button.render(target);
self.right_button.render(target);
// short-circuit before painting the main components
return;
} else {
info.info_button.render(target);
// pass through to the rest of the paint
}
}
self.message.render(target);
self.alert.render(target);
self.left_button.render(target);
self.right_button.render(target);
match &self.title {
ConfirmTitle::Text(label) => label.render(target),
ConfirmTitle::Icon(icon) => {
shape::ToifImage::new(Point::new(screen().center().x, ICON_TOP), icon.toif)
.with_align(Alignment2D::TOP_CENTER)
.with_fg(WHITE)
.render(target);
}
}
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Confirm<'_> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("BlConfirm");
}
}

View File

@ -0,0 +1,468 @@
#[cfg(feature = "haptic")]
use crate::trezorhal::haptic::{play, HapticEffect};
use crate::{
strutil::TString,
time::Duration,
ui::{
component::{Component, Event, EventCtx, Timer},
display::{toif::Icon, Color, Font},
event::TouchEvent,
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
shape::{self, Renderer},
util::split_two_lines,
},
};
use super::theme;
pub enum ButtonMsg {
Pressed,
Released,
Clicked,
LongPressed,
}
pub struct Button {
area: Rect,
touch_expand: Option<Insets>,
content: ButtonContent,
styles: ButtonStyleSheet,
text_align: Alignment,
radius: Option<u8>,
state: State,
long_press: Option<Duration>,
long_timer: Timer,
haptic: bool,
}
impl Button {
pub const BASELINE_OFFSET: Offset = Offset::new(2, 6);
pub const fn new(content: ButtonContent) -> Self {
Self {
content,
area: Rect::zero(),
touch_expand: None,
styles: theme::button_default(),
text_align: Alignment::Center,
radius: None,
state: State::Initial,
long_press: None,
long_timer: Timer::new(),
haptic: true,
}
}
pub const fn with_text(text: TString<'static>) -> Self {
Self::new(ButtonContent::Text(text))
}
pub const fn with_icon(icon: Icon) -> Self {
Self::new(ButtonContent::Icon(icon))
}
pub const fn with_icon_and_text(content: IconText) -> Self {
Self::new(ButtonContent::IconAndText(content))
}
pub const fn empty() -> Self {
Self::new(ButtonContent::Empty)
}
pub const fn styled(mut self, styles: ButtonStyleSheet) -> Self {
self.styles = styles;
self
}
pub const fn with_text_align(mut self, align: Alignment) -> Self {
self.text_align = align;
self
}
pub const fn with_expanded_touch_area(mut self, expand: Insets) -> Self {
self.touch_expand = Some(expand);
self
}
pub fn with_long_press(mut self, duration: Duration) -> Self {
self.long_press = Some(duration);
self
}
pub fn with_radius(mut self, radius: u8) -> Self {
self.radius = Some(radius);
self
}
pub fn without_haptics(mut self) -> Self {
self.haptic = false;
self
}
pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) {
if enabled {
self.enable(ctx);
} else {
self.disable(ctx);
}
}
pub fn initially_enabled(mut self, enabled: bool) -> Self {
if !enabled {
self.state = State::Disabled;
}
self
}
pub fn enable(&mut self, ctx: &mut EventCtx) {
self.set(ctx, State::Initial)
}
pub fn disable(&mut self, ctx: &mut EventCtx) {
self.set(ctx, State::Disabled)
}
pub fn is_enabled(&self) -> bool {
matches!(
self.state,
State::Initial | State::Pressed | State::Released
)
}
pub fn is_disabled(&self) -> bool {
matches!(self.state, State::Disabled)
}
pub fn set_content(&mut self, content: ButtonContent) {
if self.content != content {
self.content = content
}
}
pub fn content(&self) -> &ButtonContent {
&self.content
}
pub fn set_stylesheet(&mut self, ctx: &mut EventCtx, styles: ButtonStyleSheet) {
if self.styles != styles {
self.styles = styles;
ctx.request_paint();
}
}
pub fn style(&self) -> &ButtonStyle {
match self.state {
State::Initial | State::Released => self.styles.normal,
State::Pressed => self.styles.active,
State::Disabled => self.styles.disabled,
}
}
pub fn area(&self) -> Rect {
self.area
}
fn set(&mut self, ctx: &mut EventCtx, state: State) {
if self.state != state {
self.state = state;
ctx.request_paint();
}
}
pub fn render_background<'s>(
&self,
target: &mut impl Renderer<'s>,
style: &ButtonStyle,
alpha: u8,
) {
if self.radius.is_some() {
shape::Bar::new(self.area)
.with_bg(style.background_color)
.with_radius(self.radius.unwrap() as i16)
.with_thickness(2)
.with_fg(style.button_color)
.with_alpha(alpha)
.render(target);
} else {
shape::Bar::new(self.area)
.with_bg(style.button_color)
.with_fg(style.button_color)
.with_alpha(alpha)
.render(target);
}
}
pub fn render_content<'s>(
&self,
target: &mut impl Renderer<'s>,
style: &ButtonStyle,
alpha: u8,
) {
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => {
let y_offset = Offset::y(self.style().font.allcase_text_height() / 2);
let start_of_baseline = match self.text_align {
Alignment::Start => {
self.area.left_center() + Offset::x(Self::BASELINE_OFFSET.x)
}
Alignment::Center => self.area.center(),
Alignment::End => self.area.right_center() - Offset::x(Self::BASELINE_OFFSET.x),
} + y_offset;
text.map(|text| {
shape::Text::new(start_of_baseline, text, style.font)
.with_fg(style.text_color)
.with_align(self.text_align)
.with_alpha(alpha)
.render(target);
});
}
ButtonContent::Icon(icon) => {
shape::ToifImage::new(self.area.center(), icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(style.icon_color)
.with_alpha(alpha)
.render(target);
}
ButtonContent::IconAndText(child) => {
child.render(
target,
self.area,
self.style(),
Self::BASELINE_OFFSET,
alpha,
);
}
}
}
pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) {
let style = self.style();
self.render_background(target, style, alpha);
self.render_content(target, style, alpha);
}
}
impl Component for Button {
type Msg = ButtonMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let touch_area = if let Some(expand) = self.touch_expand {
self.area.outset(expand)
} else {
self.area
};
match event {
Event::Touch(TouchEvent::TouchStart(pos)) => {
match self.state {
State::Disabled => {
// Do nothing.
}
_ => {
// Touch started in our area, transform to `Pressed` state.
if touch_area.contains(pos) {
#[cfg(feature = "haptic")]
if self.haptic {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Pressed);
if let Some(duration) = self.long_press {
self.long_timer.start(ctx, duration);
}
return Some(ButtonMsg::Pressed);
}
}
}
}
Event::Touch(TouchEvent::TouchMove(pos)) => {
match self.state {
State::Pressed if !touch_area.contains(pos) => {
// Touch is leaving our area, transform to `Released` state.
self.set(ctx, State::Released);
return Some(ButtonMsg::Released);
}
_ => {
// Do nothing.
}
}
}
Event::Touch(TouchEvent::TouchEnd(pos)) => {
match self.state {
State::Initial | State::Disabled => {
// Do nothing.
}
State::Pressed if touch_area.contains(pos) => {
// Touch finished in our area, we got clicked.
self.set(ctx, State::Initial);
return Some(ButtonMsg::Clicked);
}
State::Pressed => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
self.long_timer.stop();
return Some(ButtonMsg::Released);
}
_ => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
self.long_timer.stop();
}
}
}
Event::Swipe(_) => {
// When a swipe is detected, abort any ongoing touch.
match self.state {
State::Initial | State::Disabled => {
// Do nothing.
}
State::Pressed => {
// Touch aborted
self.set(ctx, State::Initial);
self.long_timer.stop();
return Some(ButtonMsg::Released);
}
_ => {
// Irrelevant touch abort
self.set(ctx, State::Initial);
self.long_timer.stop();
}
}
}
Event::Timer(_) if self.long_timer.expire(event) => {
if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")]
if self.haptic {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
}
}
_ => {}
};
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = self.style();
self.render_background(target, style, 0xFF);
self.render_content(target, style, 0xFF);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Button {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Button");
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => t.string("text", *text),
ButtonContent::Icon(_) => t.bool("icon", true),
ButtonContent::IconAndText(content) => {
t.string("text", content.text);
t.bool("icon", true);
}
}
}
}
#[derive(PartialEq, Eq, Clone)]
enum State {
Initial,
Pressed,
Released,
Disabled,
}
#[derive(PartialEq, Eq, Clone)]
pub enum ButtonContent {
Empty,
Text(TString<'static>),
Icon(Icon),
IconAndText(IconText),
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub struct ButtonStyleSheet {
pub normal: &'static ButtonStyle,
pub active: &'static ButtonStyle,
pub disabled: &'static ButtonStyle,
}
#[derive(PartialEq, Eq)]
pub struct ButtonStyle {
pub font: Font,
pub text_color: Color,
pub button_color: Color,
pub icon_color: Color,
pub background_color: Color,
}
#[derive(PartialEq, Eq, Clone)]
pub struct IconText {
text: TString<'static>,
icon: Icon,
}
impl IconText {
const ICON_SPACE: i16 = 46;
const ICON_MARGIN: i16 = 4;
const TEXT_MARGIN: i16 = 6;
pub fn new(text: impl Into<TString<'static>>, icon: Icon) -> Self {
Self {
text: text.into(),
icon,
}
}
pub fn render<'s>(
&self,
target: &mut impl Renderer<'s>,
area: Rect,
style: &ButtonStyle,
baseline_offset: Offset,
alpha: u8,
) {
let mut show_text = |text: &str, rect: Rect| {
let text_pos = rect.left_center() + baseline_offset;
let text_pos = Point::new(rect.top_left().x + Self::ICON_SPACE, text_pos.y);
shape::Text::new(text_pos, text, style.font)
.with_fg(style.text_color)
.with_alpha(alpha)
.render(target)
};
self.text.map(|t| {
let (t1, t2) = split_two_lines(
t,
style.font,
area.width() - Self::ICON_SPACE - Self::TEXT_MARGIN,
);
if t1.is_empty() || t2.is_empty() {
show_text(t, area);
} else {
show_text(t1, Rect::new(area.top_left(), area.right_center()));
show_text(t2, Rect::new(area.left_center(), area.bottom_right()));
}
});
let icon_pos = Point::new(
area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2),
area.center().y,
);
shape::ToifImage::new(icon_pos, self.icon.toif)
.with_align(Alignment2D::CENTER)
.with_fg(style.icon_color)
.with_alpha(alpha)
.render(target);
}
}

View File

@ -0,0 +1,94 @@
use crate::{
strutil::TString,
ui::{
component::{Component, Event, EventCtx, Label, Never, Pad},
constant::screen,
geometry::{Alignment2D, Point, Rect},
shape,
shape::Renderer,
},
};
use super::{
constant::WIDTH,
theme::{FATAL_ERROR_COLOR, ICON_WARNING40, RESULT_FOOTER_START, RESULT_PADDING, WHITE},
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::theme::RESULT_ERROR;
pub struct ErrorScreen<'a> {
bg: Pad,
title: Label<'a>,
message: Label<'a>,
footer: ResultFooter<'a>,
}
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,
);
Self {
bg: Pad::with_background(FATAL_ERROR_COLOR).with_clear(),
title,
message,
footer,
}
}
}
impl<'a> Component for ErrorScreen<'a> {
type Msg = Never;
fn place(&mut self, _bounds: Rect) -> Rect {
self.bg.place(screen());
let title_area = Rect::new(
Point::new(RESULT_PADDING, TITLE_AREA_START),
Point::new(WIDTH - RESULT_PADDING, MESSAGE_AREA_START),
);
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()
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
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.title.render(target);
self.message.render(target);
self.footer.render(target);
}
}

View File

@ -0,0 +1,12 @@
pub mod bl_confirm;
mod button;
mod error;
mod result;
mod welcome_screen;
pub use button::{Button, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText};
pub use error::ErrorScreen;
pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use welcome_screen::WelcomeScreen;
use super::{constant, theme};

View File

@ -0,0 +1,188 @@
use crate::{
strutil::TString,
ui::{
component::{text::TextStyle, Component, Event, EventCtx, Label, Never, Pad},
constant::screen,
display::{Color, Icon},
geometry::{Alignment2D, Insets, Offset, Point, Rect},
shape,
shape::Renderer,
},
};
use super::{
super::fonts,
constant::WIDTH,
theme::{FG, RESULT_FOOTER_START, RESULT_PADDING},
};
const MESSAGE_AREA_START: i16 = 97;
const ICON_CENTER_Y: i16 = 62;
pub struct ResultStyle {
pub fg_color: Color,
pub bg_color: Color,
pub divider_color: Color,
}
impl ResultStyle {
pub const fn new(fg_color: Color, bg_color: Color, divider_color: Color) -> Self {
Self {
fg_color,
bg_color,
divider_color,
}
}
pub const fn message_style(&self) -> TextStyle {
TextStyle::new(
fonts::FONT_SATOSHI_REGULAR_38,
self.fg_color,
self.bg_color,
FG,
FG,
)
}
pub const fn title_style(&self) -> TextStyle {
TextStyle::new(
fonts::FONT_SATOSHI_MEDIUM_26,
self.fg_color,
self.bg_color,
FG,
FG,
)
}
}
pub struct ResultFooter<'a> {
style: &'a ResultStyle,
text: Label<'a>,
area: Rect,
}
impl<'a> ResultFooter<'a> {
pub fn new(text: Label<'a>, style: &'a ResultStyle) -> Self {
Self {
style,
text,
area: Rect::zero(),
}
}
}
impl ResultFooter<'_> {
pub const fn split_bounds() -> (Rect, Rect) {
let main_area = Rect::new(
Point::new(RESULT_PADDING, 0),
Point::new(WIDTH - RESULT_PADDING, RESULT_FOOTER_START),
);
let footer_area = Rect::new(
Point::new(RESULT_PADDING, RESULT_FOOTER_START),
Point::new(WIDTH - RESULT_PADDING, screen().height()),
);
(main_area, footer_area)
}
}
impl Component for ResultFooter<'_> {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.text.place(bounds);
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
// divider line
let bar = Rect::from_center_and_size(
Point::new(self.area.center().x, self.area.y0),
Offset::new(self.area.width(), 1),
);
shape::Bar::new(bar)
.with_fg(self.style.divider_color)
.render(target);
// footer text
self.text.render(target);
}
}
pub struct ResultScreen<'a> {
bg: Pad,
footer_pad: Pad,
style: &'a ResultStyle,
icon: Icon,
message: Label<'a>,
footer: ResultFooter<'a>,
}
impl<'a> ResultScreen<'a> {
pub fn new(
style: &'a ResultStyle,
icon: Icon,
message: TString<'a>,
footer: Label<'a>,
complete_draw: bool,
) -> Self {
let mut instance = Self {
bg: Pad::with_background(style.bg_color),
footer_pad: Pad::with_background(style.bg_color),
style,
icon,
message: Label::centered(message, style.message_style()),
footer: ResultFooter::new(footer, style),
};
if complete_draw {
instance.bg.clear();
} else {
instance.footer_pad.clear();
}
instance
}
}
impl<'a> Component for ResultScreen<'a> {
type Msg = Never;
fn place(&mut self, _bounds: Rect) -> Rect {
self.bg.place(screen());
let (main_area, footer_area) = ResultFooter::split_bounds();
self.footer_pad.place(footer_area);
self.footer.place(footer_area);
let message_area = main_area.inset(Insets::top(MESSAGE_AREA_START));
self.message.place(message_area);
screen()
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
self.bg.render(target);
self.footer_pad.render(target);
shape::ToifImage::new(
Point::new(screen().center().x, ICON_CENTER_Y),
self.icon.toif,
)
.with_align(Alignment2D::CENTER)
.with_fg(self.style.fg_color)
.with_bg(self.style.bg_color)
.render(target);
self.message.render(target);
self.footer.render(target);
}
}

View File

@ -0,0 +1,65 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never},
geometry::{Alignment, Alignment2D, Offset, Rect},
shape,
shape::Renderer,
};
use super::{super::fonts, theme};
const TEXT_BOTTOM_MARGIN: i16 = 54;
const ICON_TOP_MARGIN: i16 = 48;
use crate::trezorhal::model;
pub struct WelcomeScreen {
area: Rect,
}
impl WelcomeScreen {
pub fn new() -> Self {
Self { area: Rect::zero() }
}
}
impl Component for WelcomeScreen {
type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.area
}
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
None
}
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);
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);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for WelcomeScreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("WelcomeScreen");
#[cfg(not(feature = "bootloader"))]
t.string("model", model::FULL_NAME.into());
}
}

View File

@ -0,0 +1,34 @@
use crate::{
error::Error,
micropython::obj::Obj,
ui::{
component::{
text::paragraphs::{ParagraphSource, Paragraphs},
Component, Timeout,
},
layout::{obj::ComponentMsgObj, result::CANCELLED},
},
};
// Clippy/compiler complains about conflicting implementations
// TODO move the common impls to a common module
#[cfg(not(feature = "clippy"))]
impl<'a, T> ComponentMsgObj for Paragraphs<T>
where
T: ParagraphSource<'a>,
{
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
unreachable!()
}
}
// Clippy/compiler complains about conflicting implementations
#[cfg(not(feature = "clippy"))]
impl<T> ComponentMsgObj for (Timeout, T)
where
T: Component<Msg = ()>,
{
fn msg_try_into_obj(&self, _msg: Self::Msg) -> Result<Obj, Error> {
Ok(CANCELLED.as_obj())
}
}

View File

@ -0,0 +1,24 @@
use crate::ui::geometry::{Offset, Point, Rect};
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)
}
pub const SIZE: Offset = size();
pub const fn screen() -> Rect {
Rect::from_top_left_and_size(Point::zero(), SIZE)
}
pub const SCREEN: Rect = screen();

View File

@ -0,0 +1,40 @@
use crate::ui::{display::Color, geometry::Point, shape, shape::Renderer};
use super::super::constant;
pub enum LoaderRange {
Full,
FromTo(f32, f32),
}
pub fn render_loader<'s>(
center: Point,
inactive_color: Color,
active_color: Color,
background_color: Color,
range: LoaderRange,
target: &mut impl Renderer<'s>,
) {
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(inactive_color)
.render(target);
match range {
LoaderRange::Full => {
shape::Circle::new(center, constant::LOADER_OUTER)
.with_bg(active_color)
.render(target);
}
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);
}
}
shape::Circle::new(center, constant::LOADER_INNER + 2)
.with_bg(background_color)
.render(target);
}

View File

@ -0,0 +1,3 @@
mod loader;
pub use loader::{render_loader, LoaderRange};

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,72 @@
use super::{geometry::Rect, CommonUI};
use theme::backlight;
#[cfg(feature = "bootloader")]
pub mod bootloader;
pub mod component;
pub mod constant;
pub mod theme;
#[cfg(feature = "micropython")]
pub mod component_msg_obj;
pub mod cshape;
#[cfg(feature = "micropython")]
pub mod flow;
pub mod fonts;
pub mod screens;
#[cfg(feature = "micropython")]
pub mod ui_firmware;
pub struct UIEckhart;
impl CommonUI for UIEckhart {
#[cfg(feature = "backlight")]
fn fadein() {
crate::ui::display::fade_backlight_duration(backlight::get_backlight_normal(), 150);
}
#[cfg(feature = "backlight")]
fn fadeout() {
crate::ui::display::fade_backlight_duration(backlight::get_backlight_dim(), 150);
}
#[cfg(feature = "backlight")]
fn backlight_on() {
crate::ui::display::set_backlight(backlight::get_backlight_normal());
}
#[cfg(feature = "backlight")]
fn get_backlight_none() -> u8 {
backlight::get_backlight_none()
}
#[cfg(feature = "backlight")]
fn get_backlight_normal() -> u8 {
backlight::get_backlight_normal()
}
#[cfg(feature = "backlight")]
fn get_backlight_low() -> u8 {
backlight::get_backlight_low()
}
#[cfg(feature = "backlight")]
fn get_backlight_dim() -> u8 {
backlight::get_backlight_dim()
}
#[cfg(feature = "backlight")]
fn get_backlight_max() -> u8 {
backlight::get_backlight_max()
}
const SCREEN: Rect = constant::SCREEN;
fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
screens::screen_fatal_error(title, msg, footer);
}
fn screen_boot_stage_2(fade_in: bool) {
screens::screen_boot_stage_2(fade_in);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

View File

@ -0,0 +1,32 @@
use crate::ui::{component::Component, constant::screen, display};
use super::{
component::{ErrorScreen, WelcomeScreen},
constant,
};
use crate::ui::{display::Color, shape::render_on_display};
pub fn screen_fatal_error(title: &str, msg: &str, footer: &str) {
let mut frame = ErrorScreen::new(title.into(), msg.into(), footer.into());
frame.place(constant::screen());
render_on_display(None, Some(Color::black()), |target| {
frame.render(target);
});
display::refresh();
}
pub fn screen_boot_stage_2(_fade_in: bool) {
let mut frame = WelcomeScreen::new();
frame.place(screen());
display::sync();
render_on_display(None, Some(Color::black()), |target| {
frame.render(target);
});
display::refresh();
}

View File

@ -0,0 +1,50 @@
#[cfg(not(feature = "bootloader"))]
use crate::storage;
// Typical backlight values.
const BACKLIGHT_NORMAL: u8 = 150;
const BACKLIGHT_LOW: u8 = 45;
const BACKLIGHT_DIM: u8 = 5;
const BACKLIGHT_NONE: u8 = 0;
const BACKLIGHT_MIN: u8 = 10;
const BACKLIGHT_MAX: u8 = 255;
#[cfg(feature = "bootloader")]
pub fn get_backlight_normal() -> u8 {
BACKLIGHT_NORMAL
}
#[cfg(not(feature = "bootloader"))]
pub fn get_backlight_normal() -> u8 {
storage::get_brightness()
.unwrap_or(BACKLIGHT_NORMAL)
.clamp(BACKLIGHT_MIN, BACKLIGHT_MAX)
}
#[cfg(feature = "bootloader")]
pub fn get_backlight_low() -> u8 {
BACKLIGHT_LOW
}
#[cfg(not(feature = "bootloader"))]
pub fn get_backlight_low() -> u8 {
storage::get_brightness()
.unwrap_or(BACKLIGHT_LOW)
.clamp(BACKLIGHT_MIN, BACKLIGHT_LOW)
}
pub fn get_backlight_dim() -> u8 {
BACKLIGHT_DIM
}
pub fn get_backlight_none() -> u8 {
BACKLIGHT_NONE
}
pub fn get_backlight_max() -> u8 {
BACKLIGHT_MAX
}
pub fn get_backlight_min() -> u8 {
BACKLIGHT_MIN
}

View File

@ -0,0 +1,263 @@
use crate::ui::{
component::{text::TextStyle, LineBreaking::BreakWordsNoHyphen},
constant::{HEIGHT, WIDTH},
display::Color,
geometry::{Offset, Point, Rect},
util::include_res,
};
use super::super::{
component::{ButtonStyle, ButtonStyleSheet, ResultStyle},
fonts,
theme::{BLACK, FG, GREY_DARK, GREY_LIGHT, WHITE},
};
pub const BLD_BG: Color = Color::rgb(0x00, 0x1E, 0xAD);
pub const BLD_FG: Color = WHITE;
pub const BLD_WIPE_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const BLD_WIPE_TEXT_COLOR: Color = WHITE;
pub const BLD_WARN_COLOR: Color = Color::rgb(0xFF, 0x00, 0x00);
pub const BLD_WIPE_BTN_COLOR: Color = WHITE;
pub const BLD_WIPE_BTN_COLOR_ACTIVE: Color = Color::rgb(0xFA, 0xCF, 0xCF);
pub const BLD_WIPE_CANCEL_BTN_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
pub const BLD_WIPE_CANCEL_BTN_COLOR_ACTIVE: Color = Color::rgb(0xAE, 0x09, 0x09);
pub const BLD_INSTALL_BTN_COLOR_ACTIVE: Color = Color::rgb(0xCD, 0xD2, 0xEF);
pub const BLD_BTN_COLOR: Color = Color::rgb(0x2D, 0x42, 0xBF);
pub const BLD_BTN_COLOR_ACTIVE: Color = Color::rgb(0x04, 0x10, 0x58);
pub const BLD_TITLE_COLOR: Color = WHITE;
pub const WELCOME_COLOR: Color = BLACK;
pub const WELCOME_HIGHLIGHT_COLOR: Color = Color::rgb(0x28, 0x28, 0x28);
// Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2;
// Commonly used constants for UI elements.
pub const CONTENT_PADDING: i16 = 6;
pub const TITLE_AREA: Rect = Rect::new(
Point::new(CONTENT_PADDING, CONTENT_PADDING),
Point::new(WIDTH, CORNER_BUTTON_SIZE + CONTENT_PADDING),
);
pub const CORNER_BUTTON_TOUCH_EXPANSION: i16 = 13;
pub const CORNER_BUTTON_SIZE: i16 = 44;
pub const CORNER_BUTTON_AREA: Rect = Rect::from_top_left_and_size(
Point::new(
WIDTH - CORNER_BUTTON_SIZE - CONTENT_PADDING,
CONTENT_PADDING,
),
Offset::uniform(CORNER_BUTTON_SIZE),
);
pub const BUTTON_AREA_START: i16 = HEIGHT - 56;
pub const BUTTON_HEIGHT: i16 = 50;
// BLD icons
pub const X24: &[u8] = include_res!("layout_delizia/res/x24.toif");
pub const X32: &[u8] = include_res!("layout_delizia/res/x32.toif");
pub const FIRE24: &[u8] = include_res!("layout_delizia/res/fire24.toif");
pub const FIRE32: &[u8] = include_res!("layout_delizia/res/fire32.toif");
pub const FIRE40: &[u8] = include_res!("layout_delizia/res/fire40.toif");
pub const REFRESH24: &[u8] = include_res!("layout_delizia/res/refresh24.toif");
pub const MENU32: &[u8] = include_res!("layout_delizia/res/menu32.toif");
pub const INFO32: &[u8] = include_res!("layout_delizia/res/info32.toif");
pub const DOWNLOAD24: &[u8] = include_res!("layout_delizia/res/download24.toif");
pub const WARNING40: &[u8] = include_res!("layout_delizia/res/warning40.toif");
pub const CHECK24: &[u8] = include_res!("layout_delizia/res/check24.toif");
pub const CHECK40: &[u8] = include_res!("layout_delizia/res/check40.toif");
pub fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: BLD_BG,
button_color: WHITE,
icon_color: BLD_BG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: BLD_BG,
button_color: BLD_INSTALL_BTN_COLOR_ACTIVE,
icon_color: BLD_BG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: FG,
button_color: GREY_DARK,
icon_color: BLD_BG,
background_color: FG,
},
}
}
pub fn button_wipe_cancel() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: WHITE,
button_color: BLD_WIPE_CANCEL_BTN_COLOR,
icon_color: WHITE,
background_color: BLD_WIPE_COLOR,
},
active: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: WHITE,
button_color: BLD_WIPE_CANCEL_BTN_COLOR_ACTIVE,
icon_color: WHITE,
background_color: BLD_WIPE_COLOR,
},
disabled: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: GREY_DARK,
icon_color: GREY_LIGHT,
background_color: WHITE,
},
}
}
pub fn button_wipe_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: BLD_WIPE_COLOR,
button_color: BLD_WIPE_BTN_COLOR,
icon_color: BLD_WIPE_COLOR,
background_color: BLD_WIPE_COLOR,
},
active: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: BLD_WIPE_COLOR,
button_color: BLD_WIPE_BTN_COLOR_ACTIVE,
icon_color: BLD_WIPE_COLOR,
background_color: BLD_WIPE_COLOR,
},
disabled: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: FG,
button_color: GREY_DARK,
icon_color: FG,
background_color: FG,
},
}
}
pub fn button_bld_menu() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: BLD_FG,
button_color: BLD_BG,
icon_color: BLD_FG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: BLD_FG,
button_color: BLD_BG,
icon_color: BLD_FG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: BLD_BG,
icon_color: GREY_LIGHT,
background_color: BLD_BG,
},
}
}
pub fn button_bld() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: BLD_FG,
button_color: BLD_BTN_COLOR,
icon_color: BLD_FG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: BLD_FG,
button_color: BLD_BTN_COLOR_ACTIVE,
icon_color: BLD_FG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: BLD_BTN_COLOR,
icon_color: GREY_LIGHT,
background_color: BLD_BG,
},
}
}
pub const fn text_title(bg: Color) -> TextStyle {
TextStyle::new(
fonts::FONT_SATOSHI_MEDIUM_26,
BLD_TITLE_COLOR,
bg,
BLD_TITLE_COLOR,
BLD_TITLE_COLOR,
)
}
pub const TEXT_NORMAL: TextStyle = TextStyle::new(
fonts::FONT_SATOSHI_REGULAR_38,
BLD_FG,
BLD_BG,
BLD_FG,
BLD_FG,
);
pub const TEXT_WARNING: TextStyle = TextStyle::new(
fonts::FONT_SATOSHI_REGULAR_38,
BLD_WARN_COLOR,
BLD_BG,
BLD_WARN_COLOR,
BLD_WARN_COLOR,
);
pub const fn text_fingerprint(bg: Color) -> TextStyle {
TextStyle::new(fonts::FONT_SATOSHI_REGULAR_38, BLD_FG, bg, BLD_FG, BLD_FG)
.with_line_breaking(BreakWordsNoHyphen)
}
pub const TEXT_BOLD: TextStyle = TextStyle::new(
fonts::FONT_SATOSHI_REGULAR_38,
BLD_FG,
BLD_BG,
BLD_FG,
BLD_FG,
);
pub const TEXT_WIPE_BOLD: TextStyle = TextStyle::new(
fonts::FONT_SATOSHI_REGULAR_38,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_COLOR,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_TEXT_COLOR,
);
pub const TEXT_WIPE_NORMAL: TextStyle = TextStyle::new(
fonts::FONT_SATOSHI_REGULAR_38,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_COLOR,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_TEXT_COLOR,
);
pub const RESULT_WIPE: ResultStyle = ResultStyle::new(
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_COLOR,
BLD_WIPE_CANCEL_BTN_COLOR,
);
pub const RESULT_FW_INSTALL: ResultStyle = ResultStyle::new(BLD_FG, BLD_BG, BLD_BTN_COLOR);
pub const RESULT_INITIAL: ResultStyle =
ResultStyle::new(FG, WELCOME_COLOR, WELCOME_HIGHLIGHT_COLOR);

View File

@ -0,0 +1,67 @@
pub mod bootloader;
pub mod backlight;
use crate::{
time::Duration,
ui::{display::{Color, Font}, util::include_icon},
};
use super::component::{ButtonStyle, ButtonStyleSheet, ResultStyle};
pub const ERASE_HOLD_DURATION: Duration = Duration::from_millis(1500);
// Color palette.
// TODO: colors
pub const WHITE: Color = Color::rgb(0xFF, 0xFF, 0xFF);
pub const BLACK: Color = Color::rgb(0, 0, 0);
pub const FG: Color = WHITE; // Default foreground (text & icon) color.
pub const BG: Color = BLACK; // Default background color.
pub const GREY_DARK: Color = Color::rgb(0x46, 0x48, 0x4A);
pub const GREY_LIGHT: Color = Color::rgb(0xC7, 0xCD, 0xD3);
pub const FATAL_ERROR_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
// UI icons (white color).
// TODO: icons
// Welcome screen.
include_icon!(ICON_LOGO, "layout_jefferson/res/lock_full.toif");
// Homescreen notifications.
include_icon!(ICON_WARNING40, "model_jefferson/res/warning40.toif");
// TODO: button styles
pub const fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: BG,
icon_color: FG,
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: BG,
},
disabled: &ButtonStyle {
font: fonts::FONT_SATOSHI_MEDIUM_26,
text_color: GREY_LIGHT,
button_color: GREY_DARK,
icon_color: GREY_LIGHT,
background_color: GREY_DARK,
},
}
}
pub const RESULT_PADDING: i16 = 6;
pub const RESULT_FOOTER_START: i16 = 171;
pub const RESULT_FOOTER_HEIGHT: i16 = 62;
pub const RESULT_ERROR: ResultStyle =
ResultStyle::new(FG, FATAL_ERROR_COLOR, FATAL_ERROR_HIGHLIGHT_COLOR);

View File

@ -0,0 +1,481 @@
use crate::{
error::Error,
io::BinaryData,
micropython::{gc::Gc, list::List, obj::Obj},
strutil::TString,
ui::{
component::Empty,
layout::{
obj::{LayoutMaybeTrace, LayoutObj, RootComponent},
util::RecoveryType,
},
ui_firmware::{
FirmwareUI, MAX_CHECKLIST_ITEMS, MAX_GROUP_SHARE_LINES, MAX_WORD_QUIZ_ITEMS,
},
ModelUI,
},
};
use super::{
component::{ActionBar, Button, GenericScreen, Header, Hint},
theme, UIEckhart,
};
impl FirmwareUI for UIEckhart {
fn confirm_action(
_title: TString<'static>,
_action: Option<TString<'static>>,
_description: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_hold: bool,
_hold_danger: bool,
_reverse: bool,
_prompt_screen: bool,
_prompt_title: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_address(
_title: TString<'static>,
_address: Obj,
_address_label: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_info_button: bool,
_chunkify: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_homescreen(
_title: TString<'static>,
_image: BinaryData<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_coinjoin(
_max_rounds: TString<'static>,
_max_feerate: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_emphasized(
_title: TString<'static>,
_items: Obj,
_verb: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_fido(
_title: TString<'static>,
_app_name: TString<'static>,
_icon: Option<TString<'static>>,
_accounts: Gc<List>,
) -> Result<impl LayoutMaybeTrace, Error> {
#[cfg(feature = "universal_fw")]
return Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"));
#[cfg(not(feature = "universal_fw"))]
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(
c"confirm_fido not used in bitcoin-only firmware",
))
}
fn confirm_firmware_update(
_description: TString<'static>,
_fingerprint: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_modify_fee(
_title: TString<'static>,
_sign: i32,
_user_fee_change: TString<'static>,
_total_fee_new: TString<'static>,
_fee_rate_amount: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_modify_output(
_sign: i32,
_amount_change: TString<'static>,
_amount_new: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_more(
_title: TString<'static>,
_button: TString<'static>,
_button_style_confirm: bool,
_items: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_reset_device(_recovery: bool) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_summary(
_amount: TString<'static>,
_amount_label: TString<'static>,
_fee: TString<'static>,
_fee_label: TString<'static>,
_title: Option<TString<'static>>,
_account_items: Option<Obj>,
_extra_items: Option<Obj>,
_extra_title: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_properties(
_title: TString<'static>,
_items: Obj,
_hold: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_value(
_title: TString<'static>,
_value: Obj,
_description: Option<TString<'static>>,
_is_data: bool,
_extra: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_info: bool,
_hold: bool,
_chunkify: bool,
_page_counter: bool,
_prompt_screen: bool,
_cancel: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn confirm_value_intro(
_title: TString<'static>,
_value: Obj,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_chunkify: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"confirm_value_intro not implemented"))
}
fn confirm_with_info(
_title: TString<'static>,
_button: TString<'static>,
_info_button: TString<'static>,
_verb_cancel: Option<TString<'static>>,
_items: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn check_homescreen_format(_image: BinaryData, _accept_toif: bool) -> bool {
false // not implemented
}
fn continue_recovery_homepage(
_text: TString<'static>,
_subtext: Option<TString<'static>>,
_button: Option<TString<'static>>,
_recovery_type: RecoveryType,
_show_instructions: bool,
_remaining_shares: Option<Obj>,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn flow_confirm_output(
_title: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
_message: Obj,
_amount: Option<Obj>,
_chunkify: bool,
_text_mono: bool,
_account: Option<TString<'static>>,
_account_path: Option<TString<'static>>,
_br_code: u16,
_br_name: TString<'static>,
_address: Option<Obj>,
_address_title: Option<TString<'static>>,
_summary_items: Option<Obj>,
_fee_items: Option<Obj>,
_summary_title: Option<TString<'static>>,
_summary_br_code: Option<u16>,
_summary_br_name: Option<TString<'static>>,
_cancel_text: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn flow_confirm_set_new_pin(
_title: TString<'static>,
_description: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn flow_get_address(
_address: Obj,
_title: TString<'static>,
_description: Option<TString<'static>>,
_extra: Option<TString<'static>>,
_chunkify: bool,
_address_qr: TString<'static>,
_case_sensitive: bool,
_account: Option<TString<'static>>,
_path: Option<TString<'static>>,
_xpubs: Obj,
_title_success: TString<'static>,
_br_code: u16,
_br_name: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn multiple_pages_texts(
_title: TString<'static>,
_verb: TString<'static>,
_items: Gc<List>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn prompt_backup() -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_bip39(
_prompt: TString<'static>,
_prefill_word: TString<'static>,
_can_go_back: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_slip39(
_prompt: TString<'static>,
_prefill_word: TString<'static>,
_can_go_back: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_number(
_title: TString<'static>,
_count: u32,
_min_count: u32,
_max_count: u32,
_description: Option<TString<'static>>,
_more_info_callback: Option<impl Fn(u32) -> TString<'static> + 'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_pin(
_prompt: TString<'static>,
_subprompt: TString<'static>,
_allow_cancel: bool,
_warning: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn request_passphrase(
_prompt: TString<'static>,
_max_len: u32,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn select_word(
_title: TString<'static>,
_description: TString<'static>,
_words: [TString<'static>; MAX_WORD_QUIZ_ITEMS],
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn select_word_count(_recovery_type: RecoveryType) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn set_brightness(_current_brightness: Option<u8>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_address_details(
_qr_title: TString<'static>,
_address: TString<'static>,
_case_sensitive: bool,
_details_title: TString<'static>,
_account: Option<TString<'static>>,
_path: Option<TString<'static>>,
_xpubs: Obj,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_checklist(
_title: TString<'static>,
_button: TString<'static>,
_active: usize,
_items: [TString<'static>; MAX_CHECKLIST_ITEMS],
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_danger(
_title: TString<'static>,
_description: TString<'static>,
_value: TString<'static>,
_verb_cancel: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_error(
_title: TString<'static>,
_button: TString<'static>,
_description: TString<'static>,
_allow_cancel: bool,
_time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_group_share_success(
_lines: [TString<'static>; MAX_GROUP_SHARE_LINES],
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_homescreen(
_label: TString<'static>,
_hold: bool,
_notification: Option<TString<'static>>,
_notification_level: u8,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_info(
_title: TString<'static>,
_description: TString<'static>,
_button: TString<'static>,
_time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_info_with_cancel(
_title: TString<'static>,
_items: Obj,
_horizontal: bool,
_chunkify: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_lockscreen(
_label: TString<'static>,
_bootscreen: bool,
_coinjoin_authorized: bool,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_mismatch(_title: TString<'static>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_progress(
_description: TString<'static>,
_indeterminate: bool,
_title: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_progress_coinjoin(
_title: TString<'static>,
_indeterminate: bool,
_time_ms: u32,
_skip_first_paint: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_share_words(
_words: heapless::Vec<TString<'static>, 33>,
_title: Option<TString<'static>>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_share_words_delizia(
_words: heapless::Vec<TString<'static>, 33>,
_subtitle: Option<TString<'static>>,
_instructions: Obj,
_text_footer: Option<TString<'static>>,
_text_confirm: TString<'static>,
) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_remaining_shares(_pages_iterable: Obj) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_simple(
text: TString<'static>,
_title: Option<TString<'static>>,
_button: Option<TString<'static>>,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_success(
_title: TString<'static>,
_button: TString<'static>,
_description: TString<'static>,
_allow_cancel: bool,
_time_ms: u32,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn show_wait_text(_text: TString<'static>) -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
fn show_warning(
_title: TString<'static>,
_button: TString<'static>,
_value: TString<'static>,
_description: TString<'static>,
_allow_cancel: bool,
_danger: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"not implemented"))
}
fn tutorial() -> Result<impl LayoutMaybeTrace, Error> {
Err::<RootComponent<Empty, ModelUI>, Error>(Error::ValueError(c"not implemented"))
}
}

View File

@ -23,6 +23,8 @@ pub mod layout_bolt;
pub mod layout_caesar; pub mod layout_caesar;
#[cfg(feature = "layout_delizia")] #[cfg(feature = "layout_delizia")]
pub mod layout_delizia; pub mod layout_delizia;
#[cfg(feature = "layout_eckhart")]
pub mod layout_eckhart;
#[cfg(feature = "bootloader")] #[cfg(feature = "bootloader")]
pub mod ui_bootloader; pub mod ui_bootloader;
@ -40,14 +42,19 @@ pub use ui_common::CommonUI;
pub use ui_common::DebugOverlay; pub use ui_common::DebugOverlay;
#[cfg(all( #[cfg(all(
feature = "layout_delizia", feature = "layout_bolt",
not(feature = "layout_caesar"), not(feature = "layout_caesar"),
not(feature = "layout_bolt") not(feature = "layout_delizia"),
not(feature = "layout_eckhart")
))] ))]
pub type ModelUI = crate::ui::layout_delizia::UIDelizia;
#[cfg(all(feature = "layout_caesar", not(feature = "layout_bolt")))]
pub type ModelUI = crate::ui::layout_caesar::UICaesar;
#[cfg(feature = "layout_bolt")]
pub type ModelUI = crate::ui::layout_bolt::UIBolt; pub type ModelUI = crate::ui::layout_bolt::UIBolt;
#[cfg(all(
feature = "layout_caesar",
not(feature = "layout_delizia"),
not(feature = "layout_eckhart")
))]
pub type ModelUI = crate::ui::layout_caesar::UICaesar;
#[cfg(all(feature = "layout_delizia", not(feature = "layout_eckhart")))]
pub type ModelUI = crate::ui::layout_delizia::UIDelizia;
#[cfg(feature = "layout_eckhart")]
pub type ModelUI = crate::ui::layout_eckhart::UIEckhart;

View File

@ -514,6 +514,8 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_CAESAR)}, {MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_CAESAR)},
#elif UI_LAYOUT_DELIZIA #elif UI_LAYOUT_DELIZIA
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_DELIZIA)}, {MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_DELIZIA)},
#elif UI_LAYOUT_ECKHART
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_ECKHART)},
#else #else
#error Unknown layout #error Unknown layout
#endif #endif

View File

@ -36,7 +36,7 @@ def configure_board(
def get_model_ui() -> str: def get_model_ui() -> str:
return "bolt" return "eckhart"
def get_model_ui_conf() -> list[str]: def get_model_ui_conf() -> list[str]:

View File

@ -49,6 +49,7 @@ def generate(env):
layout_bolt = env["ui_layout"] == "UI_LAYOUT_BOLT" layout_bolt = env["ui_layout"] == "UI_LAYOUT_BOLT"
layout_caesar = env["ui_layout"] == "UI_LAYOUT_CAESAR" layout_caesar = env["ui_layout"] == "UI_LAYOUT_CAESAR"
layout_delizia = env["ui_layout"] == "UI_LAYOUT_DELIZIA" layout_delizia = env["ui_layout"] == "UI_LAYOUT_DELIZIA"
layout_eckhart = env["ui_layout"] == "UI_LAYOUT_ECKHART"
thp = env["thp"] thp = env["thp"]
interim = f"{target[:-4]}.i" # replace .mpy with .i interim = f"{target[:-4]}.i" # replace .mpy with .i
sed_scripts = [ sed_scripts = [
@ -58,6 +59,7 @@ def generate(env):
rf"-e 's/utils\.UI_LAYOUT == \"BOLT\"/{layout_bolt}/g'", rf"-e 's/utils\.UI_LAYOUT == \"BOLT\"/{layout_bolt}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"CAESAR\"/{layout_caesar}/g'", rf"-e 's/utils\.UI_LAYOUT == \"CAESAR\"/{layout_caesar}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"DELIZIA\"/{layout_delizia}/g'", rf"-e 's/utils\.UI_LAYOUT == \"DELIZIA\"/{layout_delizia}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"ECKHART\"/{layout_eckhart}/g'",
rf"-e 's/utils\.USE_BUTTON/{button}/g'", rf"-e 's/utils\.USE_BUTTON/{button}/g'",
rf"-e 's/utils\.USE_TOUCH/{touch}/g'", rf"-e 's/utils\.USE_TOUCH/{touch}/g'",
rf"-e 's/utils\.USE_THP/{thp}/g'", rf"-e 's/utils\.USE_THP/{thp}/g'",

View File

@ -2,11 +2,12 @@ from __future__ import annotations
from site_scons import models from site_scons import models
from . import ui_bolt, ui_caesar, ui_delizia from . import ui_bolt, ui_caesar, ui_delizia, ui_eckhart
def get_ui_module(model: str, stage: str): def get_ui_module(model: str, stage: str):
ui_modules = { ui_modules = {
"eckhart": ui_eckhart,
"delizia": ui_delizia, "delizia": ui_delizia,
"caesar": ui_caesar, "caesar": ui_caesar,
"bolt": ui_bolt, "bolt": ui_bolt,

View File

@ -0,0 +1,24 @@
from __future__ import annotations
def init_ui(
stage: str,
config: list[str],
rust_features: list[str],
):
rust_features.append("layout_eckhart")
if stage == "firmware":
rust_features.append("ui_blurring")
rust_features.append("ui_jpeg")
rust_features.append("ui_image_buffer")
rust_features.append("ui_overlay")
def get_ui_layout() -> str:
return "UI_LAYOUT_ECKHART"
def get_ui_layout_path() -> str:
return "trezor/ui/layouts/eckhart/"

View File

@ -193,6 +193,14 @@ trezor.ui.layouts.delizia.recovery
import trezor.ui.layouts.delizia.recovery import trezor.ui.layouts.delizia.recovery
trezor.ui.layouts.delizia.reset trezor.ui.layouts.delizia.reset
import trezor.ui.layouts.delizia.reset import trezor.ui.layouts.delizia.reset
trezor.ui.layouts.eckhart
import trezor.ui.layouts.eckhart
trezor.ui.layouts.eckhart.fido
import trezor.ui.layouts.eckhart.fido
trezor.ui.layouts.eckhart.recovery
import trezor.ui.layouts.eckhart.recovery
trezor.ui.layouts.eckhart.reset
import trezor.ui.layouts.eckhart.reset
trezor.ui.layouts.fido trezor.ui.layouts.fido
import trezor.ui.layouts.fido import trezor.ui.layouts.fido
trezor.ui.layouts.homescreen trezor.ui.layouts.homescreen

View File

@ -10,5 +10,7 @@ elif utils.UI_LAYOUT == "CAESAR":
from .caesar import * # noqa: F401,F403 from .caesar import * # noqa: F401,F403
elif utils.UI_LAYOUT == "DELIZIA": elif utils.UI_LAYOUT == "DELIZIA":
from .delizia import * # noqa: F401,F403 from .delizia import * # noqa: F401,F403
elif utils.UI_LAYOUT == "ECKHART":
from .eckhart import * # noqa: F401,F403
else: else:
raise ValueError("Unknown layout") raise ValueError("Unknown layout")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
import trezorui_api
from trezor import ui
from trezor.enums import ButtonRequestType
from ..common import interact
async def confirm_fido(
header: str,
app_name: str,
icon_name: str | None,
accounts: list[str | None],
) -> int:
"""Webauthn confirmation for one or more credentials."""
confirm = trezorui_api.confirm_fido(
title=header,
app_name=app_name,
icon_name=icon_name,
accounts=accounts,
)
result = await interact(confirm, "confirm_fido", ButtonRequestType.Other)
if __debug__ and result is trezorui_api.CONFIRMED:
# debuglink will directly inject a CONFIRMED message which we need to handle
# by playing back a click to the Rust layout and getting out the selected number
# that way
# Or we can just return 0 because this only happens in U2F tests
# which don't use multiple credentials.
return 0
# The Rust side returns either an int or `CANCELLED`. We detect the int situation
# and assume cancellation otherwise.
if isinstance(result, int):
return result
# Late import won't get executed on the happy path.
from trezor.wire import ActionCancelled
raise ActionCancelled
async def confirm_fido_reset() -> bool:
from trezor import TR
confirm = ui.Layout(
trezorui_api.confirm_action(
title=TR.fido__title_reset,
action=TR.fido__erase_credentials,
description=TR.words__really_wanna,
reverse=True,
prompt_screen=True,
)
)
return (await confirm.get_result()) is trezorui_api.CONFIRMED

View File

@ -0,0 +1,147 @@
from typing import TYPE_CHECKING
import trezorui_api
from trezor import TR
from trezor.enums import ButtonRequestType, RecoveryType
from ..common import interact
from . import raise_if_not_confirmed
CONFIRMED = trezorui_api.CONFIRMED # global_import_cache
CANCELLED = trezorui_api.CANCELLED # global_import_cache
INFO = trezorui_api.INFO # global_import_cache
if TYPE_CHECKING:
from apps.management.recovery_device.layout import RemainingSharesInfo
async def request_word_count(recovery_type: RecoveryType) -> int:
count = await interact(
trezorui_api.select_word_count(recovery_type=recovery_type),
"recovery_word_count",
ButtonRequestType.MnemonicWordCount,
)
return int(count)
async def request_word(
word_index: int,
word_count: int,
is_slip39: bool,
send_button_request: bool,
prefill_word: str = "",
) -> str:
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
can_go_back = word_index > 0
if is_slip39:
keyboard = trezorui_api.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
else:
keyboard = trezorui_api.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
word: str = await interact(
keyboard,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
return word
def format_remaining_shares_info(
remaining_shares_info: "RemainingSharesInfo",
) -> list[tuple[str, str]]:
from trezor import strings
from trezor.crypto.slip39 import MAX_SHARE_COUNT
groups, shares_remaining, group_threshold = remaining_shares_info
pages: list[tuple[str, str]] = []
completed_groups = shares_remaining.count(0)
for group, remaining in zip(groups, shares_remaining):
if 0 < remaining < MAX_SHARE_COUNT:
title = strings.format_plural(
TR.recovery__x_more_items_starting_template_plural,
remaining,
TR.plurals__x_shares_needed,
)
words = "\n".join(group)
pages.append((title, words))
elif remaining == MAX_SHARE_COUNT and completed_groups < group_threshold:
groups_remaining = group_threshold - completed_groups
title = strings.format_plural(
TR.recovery__x_more_items_starting_template_plural,
groups_remaining,
TR.plurals__x_groups_needed,
)
words = "\n".join(group)
pages.append((title, words))
return pages
async def show_group_share_success(share_index: int, group_index: int) -> None:
await raise_if_not_confirmed(
trezorui_api.show_group_share_success(
lines=[
TR.recovery__you_have_entered,
TR.recovery__share_num_template.format(share_index + 1),
TR.words__from,
TR.recovery__group_num_template.format(group_index + 1),
],
),
"share_success",
ButtonRequestType.Other,
)
async def continue_recovery(
_button_label: str, # unused on mercury
text: str,
subtext: str | None,
recovery_type: RecoveryType,
show_instructions: bool = False,
remaining_shares_info: "RemainingSharesInfo | None" = None,
) -> bool:
result = await interact(
trezorui_api.continue_recovery_homepage(
text=text,
subtext=subtext,
button=None,
recovery_type=recovery_type,
show_instructions=show_instructions,
remaining_shares=(
format_remaining_shares_info(remaining_shares_info)
if remaining_shares_info
else None
),
),
None,
ButtonRequestType.Other,
raise_on_cancel=None,
)
return result is CONFIRMED
async def show_recovery_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> None:
button = button or TR.buttons__try_again # def_arg
await raise_if_not_confirmed(
trezorui_api.show_warning(
title=content or TR.words__warning,
value=subheader or "",
button=button,
description="",
danger=True,
),
br_name,
br_code,
)

View File

@ -0,0 +1,356 @@
from typing import Awaitable, Callable, Sequence
import trezorui_api
from trezor import TR, ui
from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
from ..common import interact
from . import raise_if_not_confirmed, show_success
CONFIRMED = trezorui_api.CONFIRMED # global_import_cache
def show_share_words(
share_words: Sequence[str],
share_index: int | None = None,
group_index: int | None = None,
) -> Awaitable[None]:
# FIXME: not implemented
raise NotImplemented
async def select_word(
words: Sequence[str],
share_index: int | None,
checked_index: int,
count: int,
group_index: int | None = None,
) -> str:
if share_index is None:
title: str = TR.reset__check_wallet_backup_title
elif group_index is None:
title: str = TR.reset__check_share_title_template.format(share_index + 1)
else:
title: str = TR.reset__check_group_share_title_template.format(
group_index + 1, share_index + 1
)
# It may happen (with a very low probability)
# that there will be less than three unique words to choose from.
# In that case, duplicating the last word to make it three.
words = list(words)
while len(words) < 3:
words.append(words[-1])
result = await interact(
trezorui_api.select_word(
title=title,
description=TR.reset__select_word_x_of_y_template.format(
checked_index + 1, count
),
words=(words[0], words[1], words[2]),
),
None,
)
if __debug__ and isinstance(result, str):
return result
assert isinstance(result, int) and 0 <= result <= 2
return words[result]
async def slip39_show_checklist(
step: int,
advanced: bool,
count: int | None = None,
threshold: int | None = None,
) -> None:
items = _slip_39_checklist_items(step, advanced, count, threshold)
result = await interact(
trezorui_api.show_checklist(
title=TR.reset__title_shamir_backup,
button=TR.buttons__continue,
active=step,
items=items,
),
"slip39_checklist",
ButtonRequestType.ResetDevice,
)
if result != CONFIRMED:
raise ActionCancelled
def _slip_39_checklist_items(
step: int,
advanced: bool,
count: int | None = None,
threshold: int | None = None,
) -> tuple[str, str, str]:
if not advanced:
entry_1 = (
TR.reset__slip39_checklist_num_shares_x_template.format(count)
if count
else TR.reset__slip39_checklist_set_num_shares
)
entry_2 = (
TR.reset__slip39_checklist_threshold_x_template.format(threshold)
if threshold
else TR.reset__slip39_checklist_set_threshold
)
entry_3 = TR.reset__slip39_checklist_write_down_recovery
return (entry_1, entry_2, entry_3)
else:
entry_1 = (
TR.reset__slip39_checklist_num_groups_x_template.format(count)
if count
else TR.reset__slip39_checklist_set_num_groups
)
entry_2 = (
TR.reset__slip39_checklist_threshold_x_template.format(threshold)
if threshold
else TR.reset__slip39_checklist_set_threshold
)
entry_3 = TR.reset__slip39_checklist_set_sizes_longer
return (entry_1, entry_2, entry_3)
async def _prompt_number(
title: str,
description: str,
info: Callable[[int], str],
count: int,
min_count: int,
max_count: int,
br_name: str,
) -> int:
result = await interact(
trezorui_api.request_number(
title=title,
count=count,
min_count=min_count,
max_count=max_count,
description=description,
more_info_callback=info,
),
br_name,
ButtonRequestType.ResetDevice,
raise_on_cancel=None,
)
if __debug__ and result is CONFIRMED:
# sent by debuglink. debuglink does not change the number of shares anyway
# so use the initial one
return count
if result is not trezorui_api.CANCELLED:
assert isinstance(result, int)
return result
else:
raise ActionCancelled # user cancelled request number prompt
def slip39_prompt_threshold(
num_of_shares: int, group_id: int | None = None
) -> Awaitable[int]:
count = num_of_shares // 2 + 1
# min value of share threshold is 2 unless the number of shares is 1
# number of shares 1 is possible in advanced slip39
min_count = min(2, num_of_shares)
max_count = num_of_shares
description = (
TR.reset__select_threshold
if group_id is None
else TR.reset__num_shares_for_group_template.format(group_id + 1)
)
def info(count: int) -> str:
return (
TR.reset__slip39_checklist_more_info_threshold
+ "\n"
+ TR.reset__slip39_checklist_more_info_threshold_example_template.format(
count, num_of_shares, count
)
)
return _prompt_number(
TR.reset__title_set_threshold,
description,
info,
count,
min_count,
max_count,
"slip39_threshold",
)
async def slip39_prompt_number_of_shares(
num_words: int, group_id: int | None = None
) -> int:
count = 5
min_count = 1
max_count = 16
description = (
TR.reset__num_of_shares_how_many
if group_id is None
else TR.reset__total_number_of_shares_in_group_template.format(group_id + 1)
)
if group_id is None:
info = TR.reset__num_of_shares_long_info_template.format(num_words)
else:
info = TR.reset__num_of_shares_advanced_info_template.format(
num_words, group_id + 1
)
return await _prompt_number(
TR.reset__title_set_number_of_shares,
description,
lambda i: info,
count,
min_count,
max_count,
"slip39_shares",
)
async def slip39_advanced_prompt_number_of_groups() -> int:
count = 5
min_count = 2
max_count = 16
description = TR.reset__group_description
info = TR.reset__group_info
return await _prompt_number(
TR.reset__title_set_number_of_groups,
description,
lambda i: info,
count,
min_count,
max_count,
"slip39_groups",
)
async def slip39_advanced_prompt_group_threshold(num_of_groups: int) -> int:
count = num_of_groups // 2 + 1
min_count = 1
max_count = num_of_groups
description = TR.reset__required_number_of_groups
info = TR.reset__advanced_group_threshold_info
return await _prompt_number(
TR.reset__title_set_group_threshold,
description,
lambda i: info,
count,
min_count,
max_count,
"slip39_group_threshold",
)
async def show_intro_backup(single_share: bool, num_of_words: int | None) -> None:
if single_share:
assert num_of_words is not None
description = TR.backup__info_single_share_backup.format(num_of_words)
else:
description = TR.backup__info_multi_share_backup
await interact(
trezorui_api.show_info(
title=TR.backup__title_create_wallet_backup,
description=description,
),
"backup_intro",
ButtonRequestType.ResetDevice,
)
def show_warning_backup() -> Awaitable[ui.UiResult]:
return interact(
trezorui_api.show_warning(
title=TR.words__important,
value=TR.reset__never_make_digital_copy,
button="",
allow_cancel=False,
danger=False, # Use a less severe icon color
),
"backup_warning",
ButtonRequestType.ResetDevice,
)
def show_success_backup() -> Awaitable[None]:
return show_success(
"success_backup",
TR.backup__title_backup_completed,
)
def show_reset_warning(
br_name: str,
content: str,
subheader: str | None = None,
button: str | None = None,
br_code: ButtonRequestType = ButtonRequestType.Warning,
) -> Awaitable[None]:
return raise_if_not_confirmed(
trezorui_api.show_warning(
title=subheader or "",
description=content,
value="",
button="",
allow_cancel=False,
danger=True,
),
br_name,
br_code,
)
async def show_share_confirmation_success(
share_index: int | None = None,
num_of_shares: int | None = None,
group_index: int | None = None,
) -> None:
if share_index is None or num_of_shares is None:
# it is a BIP39 or a 1-of-1 SLIP39 backup
# mercury UI shows only final wallet backup confirmation screen later
return
# TODO: super-shamir copy not done
if share_index == num_of_shares - 1:
title = TR.reset__share_completed_template.format(share_index + 1)
if group_index is None:
footer_description = ""
else:
footer_description = TR.reset__finished_verifying_group_template.format(
group_index + 1
)
else:
if group_index is None:
title = TR.reset__share_completed_template.format(share_index + 1)
footer_description = (
TR.instructions__shares_continue_with_x_template.format(share_index + 2)
)
else:
title = TR.reset__continue_with_next_share
footer_description = (
TR.reset__group_share_checked_successfully_template.format(
group_index + 1, share_index + 1
)
)
await show_success("success_recovery", title, subheader=footer_description)
def show_share_confirmation_failure() -> Awaitable[None]:
return show_reset_warning(
"warning_backup_check",
TR.words__try_again,
TR.reset__incorrect_word_selected,
"",
ButtonRequestType.ResetDevice,
)

View File

@ -6,3 +6,5 @@ elif utils.UI_LAYOUT == "CAESAR":
from .caesar.fido import * # noqa: F401,F403 from .caesar.fido import * # noqa: F401,F403
elif utils.UI_LAYOUT == "DELIZIA": elif utils.UI_LAYOUT == "DELIZIA":
from .delizia.fido import * # noqa: F401,F403 from .delizia.fido import * # noqa: F401,F403
elif utils.UI_LAYOUT == "ECKHART":
from .eckhart.fido import * # noqa: F401,F403

View File

@ -6,3 +6,5 @@ elif utils.UI_LAYOUT == "CAESAR":
from .caesar.recovery import * # noqa: F401,F403 from .caesar.recovery import * # noqa: F401,F403
elif utils.UI_LAYOUT == "DELIZIA": elif utils.UI_LAYOUT == "DELIZIA":
from .delizia.recovery import * # noqa: F401,F403 from .delizia.recovery import * # noqa: F401,F403
elif utils.UI_LAYOUT == "ECKHART":
from .eckhart.recovery import * # noqa: F401,F403

View File

@ -6,3 +6,5 @@ elif utils.UI_LAYOUT == "CAESAR":
from .caesar.reset import * # noqa: F401,F403 from .caesar.reset import * # noqa: F401,F403
elif utils.UI_LAYOUT == "DELIZIA": elif utils.UI_LAYOUT == "DELIZIA":
from .delizia.reset import * # noqa: F401,F403 from .delizia.reset import * # noqa: F401,F403
elif utils.UI_LAYOUT == "ECKHART":
from .eckhart.reset import * # noqa: F401,F403

View File

@ -50,6 +50,7 @@ MODEL_CHOICE = ChoiceType(
"T2B1": models.T2B1, "T2B1": models.T2B1,
"T3T1": models.T3T1, "T3T1": models.T3T1,
"T3B1": models.T3B1, "T3B1": models.T3B1,
"T3W1": models.T3W1,
# aliases # aliases
"1": models.T1B1, "1": models.T1B1,
"one": models.T1B1, "one": models.T1B1,

View File

@ -93,6 +93,8 @@ class LayoutType(Enum):
return cls.Eckhart return cls.Eckhart
if model in (models.T1B1,): if model in (models.T1B1,):
return cls.T1 return cls.T1
if model in (models.T3W1,):
return cls.Eckhart
raise ValueError(f"Unknown model: {model}") raise ValueError(f"Unknown model: {model}")

View File

@ -41,13 +41,14 @@ def test_hold_to_lock(device_handler: "BackgroundDeviceHandler"):
models.T3B1: 500, models.T3B1: 500,
models.T2T1: 1000, models.T2T1: 1000,
models.T3T1: 1000, models.T3T1: 1000,
models.T3W1: 1000,
}[debug.model] }[debug.model]
lock_duration = { lock_duration = {
models.T1B1: 1200, models.T1B1: 1200,
models.T2B1: 1200, models.T2B1: 1200,
models.T3B1: 1200, models.T3B1: 1200,
models.T2T1: 3500, models.T2T1: 3500,
models.T3T1: 3500, models.T3W1: 3500,
}[debug.model] }[debug.model]
def hold(duration: int) -> None: def hold(duration: int) -> None:

View File

@ -177,6 +177,7 @@ class ModelsFilter:
"safe3": {models.T2B1, models.T3B1}, "safe3": {models.T2B1, models.T3B1},
"safe5": {models.T3T1}, "safe5": {models.T3T1},
"delizia": {models.T3T1}, "delizia": {models.T3T1},
"eckhart": {models.T3W1},
} }
def __init__(self, node: Node) -> None: def __init__(self, node: Node) -> None:

View File

@ -12,6 +12,7 @@ FIRMWARE_LENGTHS = {
models.T2B1: 13 * 128 * 1024, models.T2B1: 13 * 128 * 1024,
models.T3T1: 208 * 8 * 1024, models.T3T1: 208 * 8 * 1024,
models.T3B1: 208 * 8 * 1024, models.T3B1: 208 * 8 * 1024,
models.T3W1: 208 * 8 * 1024, # FIXME: fill in the correct value
} }

View File

@ -43,6 +43,7 @@ MAX_DATA_LENGTH = {
models.T2B1: 32 * 1024, models.T2B1: 32 * 1024,
models.T3T1: 256 * 1024, models.T3T1: 256 * 1024,
models.T3B1: 256 * 1024, models.T3B1: 256 * 1024,
models.T3W1: 256 * 1024, # FIXME: fill in correct value
} }