1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-01-03 03:50:58 +00:00

feat(lincoln): initial commit

- build variables, lincoln UI selected for compilation only with
`UI_LINCOLN_DEV=1`
- lincoln directory structure
- bootloader UI and assets copied from mercury
- FirmwareUI trait functions are empty
- Python layout functions are copied from mercury 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 7eb0b4d689
commit 7f240247a4
62 changed files with 4427 additions and 22 deletions

View File

@ -33,6 +33,7 @@ BOOTLOADER_QA ?= 0
BOOTLOADER_DEVEL ?= 0
DISABLE_OPTIGA ?= 0
TREZOR_MODEL ?= T
UI_LINCOLN_DEV ?= 0
TREZOR_MEMPERF ?= 0
ADDRESS_SANITIZER ?= 0
CMAKELISTS ?= 0
@ -69,7 +70,12 @@ MODEL_FEATURE = model_tr
else ifeq ($(TREZOR_MODEL),$(filter $(TREZOR_MODEL),T3W1))
MCU = STM32U5
OPENOCD_TARGET = target/stm32u5x.cfg
# model_tt by default so far, lincoln if requested
ifeq ($(UI_LINCOLN_DEV),1)
MODEL_FEATURE = model_lincoln
else
MODEL_FEATURE = model_tt
endif
else ifeq ($(TREZOR_MODEL),$(filter $(TREZOR_MODEL),DISC1))
MCU = STM32F4
OPENOCD_TARGET = target/stm32f4x.cfg
@ -151,7 +157,8 @@ SCONS_VARS = \
TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" \
TREZOR_EMULATOR_DEBUGGABLE=$(TREZOR_EMULATOR_DEBUGGABLE) \
TREZOR_MEMPERF="$(TREZOR_MEMPERF)" \
TREZOR_MODEL="$(TREZOR_MODEL)"
TREZOR_MODEL="$(TREZOR_MODEL)" \
UI_LINCOLN_DEV="$(UI_LINCOLN_DEV)"
SCONS_OPTS = -Q -j $(JOBS)
ifeq ($(QUIET_MODE),1)

View File

@ -11,6 +11,7 @@ crypto = ["zeroize"]
model_tt = ["jpeg"]
model_tr = []
model_mercury = ["jpeg", "dma2d"]
model_lincoln = ["jpeg", "dma2d"]
micropython = []
protobuf = ["micropython"]
ui = []

View File

@ -86,12 +86,24 @@ const DEFAULT_BINDGEN_MACROS_T3T1: &[&str] = &[
#[cfg(not(feature = "model_mercury"))]
const DEFAULT_BINDGEN_MACROS_T3T1: &[&str] = &[];
#[cfg(feature = "model_lincoln")]
const DEFAULT_BINDGEN_MACROS_T3W1: &[&str] = &[
"-DSTM32U5",
"-DTREZOR_MODEL_T3W1",
"-DFLASH_BIT_ACCESS=0", // FIXME: fill in correct value
"-DFLASH_BLOCK_WORDS=4",
"-DTREZOR_BOARD=\"T3W1/boards/t3w1-unix.h\"",
];
#[cfg(not(feature = "model_lincoln"))]
const DEFAULT_BINDGEN_MACROS_T3W1: &[&str] = &[];
fn add_bindgen_macros<'a>(clang_args: &mut Vec<&'a str>, envvar: Option<&'a str>) {
let default_macros = DEFAULT_BINDGEN_MACROS_COMMON
.iter()
.chain(DEFAULT_BINDGEN_MACROS_T2T1)
.chain(DEFAULT_BINDGEN_MACROS_T2B1)
.chain(DEFAULT_BINDGEN_MACROS_T3T1);
.chain(DEFAULT_BINDGEN_MACROS_T3T1)
.chain(DEFAULT_BINDGEN_MACROS_T3W1);
match envvar {
Some(envvar) => clang_args.extend(envvar.split(',')),

View File

@ -1,10 +1,14 @@
//! Reexporting the `constant` module according to the
//! current feature (Trezor model)
#[cfg(all(
feature = "model_lincoln",
not(any(feature = "model_mercury", feature = "model_tr", feature = "model_tt"))
))]
pub use super::model_lincoln::constant::*;
#[cfg(all(
feature = "model_mercury",
not(feature = "model_tr"),
not(feature = "model_tt")
not(any(feature = "model_tr", feature = "model_tt"),)
))]
pub use super::model_mercury::constant::*;
#[cfg(all(feature = "model_tr", not(feature = "model_tt")))]

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
/// dimensions of objects. Absolute positions on the screen are represented by
/// 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)]
pub struct Offset {
pub x: i16,

View File

@ -17,6 +17,8 @@ pub mod layout;
mod api;
#[cfg(feature = "model_lincoln")]
pub mod model_lincoln;
#[cfg(feature = "model_mercury")]
pub mod model_mercury;
#[cfg(feature = "model_tr")]
@ -32,10 +34,15 @@ pub mod ui_firmware;
pub use ui_common::CommonUI;
#[cfg(all(
feature = "model_lincoln",
not(any(feature = "model_mercury", feature = "model_tr", feature = "model_tt"))
))]
pub type ModelUI = crate::ui::model_lincoln::UILincoln;
#[cfg(all(
feature = "model_mercury",
not(feature = "model_tr"),
not(feature = "model_tt")
not(any(feature = "model_tr", feature = "model_tt"))
))]
pub type ModelUI = crate::ui::model_mercury::UIMercury;

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,473 @@
use heapless::String;
use crate::{
trezorhal::secbool::secbool,
ui::{
component::{connect::Connect, Label},
display::{self, Color, Font, Icon},
geometry::{Alignment, Offset, Point, Rect},
layout::simplified::{run, show},
},
};
use super::{
bootloader::welcome::Welcome,
component::{
bl_confirm::{Confirm, ConfirmTitle},
Button, ResultScreen, WelcomeScreen,
},
theme,
theme::{
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,
},
UILincoln,
};
use crate::ui::{ui_bootloader::BootloaderUI, CommonUI};
use crate::ui::{
display::{toif::Toif, LOADER_MAX},
geometry::Alignment2D,
model_mercury::cshape::{render_loader, LoaderRange},
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 = UILincoln::SCREEN;
const PROGRESS_TEXT_ORIGIN: Point = Point::new(2, 28);
impl UILincoln {
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)
.with_font(Font::NORMAL)
.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,
)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.with_fg(GREY)
.render(target);
}
});
display::refresh();
if initialize {
Self::fadein();
}
}
}
impl BootloaderUI for UILincoln {
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(theme::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...", 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 { BLD_BG };
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)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.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())
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.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())
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.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 ...")
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.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,470 @@
#[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::Start,
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)
.with_font(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)
.with_font(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::model_mercury::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,11 @@
mod button;
mod error;
mod result;
mod welcome_screen;
pub use button::{ButtonStyle, ButtonStyleSheet};
pub use error::ErrorScreen;
pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use welcome_screen::WelcomeScreen;
use super::{constant, theme};

View File

@ -0,0 +1,175 @@
use crate::{
strutil::TString,
ui::{
component::{text::TextStyle, Component, Event, EventCtx, Label, Never, Pad},
constant::screen,
display::{Color, Font, Icon},
geometry::{Alignment2D, Insets, Offset, Point, Rect},
shape,
shape::Renderer,
},
};
use super::{
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(Font::NORMAL, self.fg_color, self.bg_color, FG, FG)
}
pub const fn title_style(&self) -> TextStyle {
TextStyle::new(Font::BOLD, 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,68 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never},
display::font::Font,
geometry::{Alignment, Alignment2D, Offset, Rect},
shape,
shape::Renderer,
};
use super::theme;
const TEXT_BOTTOM_MARGIN: i16 = 54;
const ICON_TOP_MARGIN: i16 = 48;
#[cfg(not(feature = "bootloader"))]
const MODEL_NAME_FONT: Font = Font::DEMIBOLD;
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,
)
.with_align(Alignment::Center)
.with_font(Font::NORMAL)
.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,38 @@
use crate::ui::{display::Color, geometry::Point, model_lincoln::constant, shape, shape::Renderer};
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,71 @@
use super::{geometry::Rect, CommonUI};
use crate::ui::model_lincoln::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 screens;
#[cfg(feature = "micropython")]
pub mod ui_firmware;
pub struct UILincoln;
impl CommonUI for UILincoln {
#[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() {
screens::screen_boot_stage_2();
}
}

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() {
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,249 @@
use crate::ui::{
component::{text::TextStyle, LineBreaking::BreakWordsNoHyphen},
constant::{HEIGHT, WIDTH},
display::{Color, Font},
geometry::{Offset, Point, Rect},
util::include_res,
};
use super::super::{
component::{ButtonStyle, ButtonStyleSheet, ResultStyle},
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!("model_mercury/res/x24.toif");
pub const X32: &[u8] = include_res!("model_mercury/res/x32.toif");
pub const FIRE24: &[u8] = include_res!("model_mercury/res/fire24.toif");
pub const FIRE32: &[u8] = include_res!("model_mercury/res/fire32.toif");
pub const FIRE40: &[u8] = include_res!("model_mercury/res/fire40.toif");
pub const REFRESH24: &[u8] = include_res!("model_mercury/res/refresh24.toif");
pub const MENU32: &[u8] = include_res!("model_mercury/res/menu32.toif");
pub const INFO32: &[u8] = include_res!("model_mercury/res/info32.toif");
pub const DOWNLOAD24: &[u8] = include_res!("model_mercury/res/download24.toif");
pub const WARNING40: &[u8] = include_res!("model_mercury/res/warning40.toif");
pub const CHECK24: &[u8] = include_res!("model_mercury/res/check24.toif");
pub const CHECK40: &[u8] = include_res!("model_mercury/res/check40.toif");
pub fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_BG,
button_color: WHITE,
icon_color: BLD_BG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_BG,
button_color: BLD_INSTALL_BTN_COLOR_ACTIVE,
icon_color: BLD_BG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: GREY_DARK,
icon_color: BLD_BG,
background_color: FG,
},
}
}
pub fn button_wipe_cancel() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: WHITE,
button_color: BLD_WIPE_CANCEL_BTN_COLOR,
icon_color: WHITE,
background_color: BLD_WIPE_COLOR,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: WHITE,
button_color: BLD_WIPE_CANCEL_BTN_COLOR_ACTIVE,
icon_color: WHITE,
background_color: BLD_WIPE_COLOR,
},
disabled: &ButtonStyle {
font: Font::BOLD,
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: Font::BOLD,
text_color: BLD_WIPE_COLOR,
button_color: BLD_WIPE_BTN_COLOR,
icon_color: BLD_WIPE_COLOR,
background_color: BLD_WIPE_COLOR,
},
active: &ButtonStyle {
font: Font::BOLD,
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: Font::BOLD,
text_color: FG,
button_color: GREY_DARK,
icon_color: FG,
background_color: FG,
},
}
}
pub fn button_bld_menu() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BG,
icon_color: BLD_FG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BG,
icon_color: BLD_FG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: Font::BOLD,
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: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BTN_COLOR,
icon_color: BLD_FG,
background_color: BLD_BG,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BTN_COLOR_ACTIVE,
icon_color: BLD_FG,
background_color: BLD_BG,
},
disabled: &ButtonStyle {
font: Font::BOLD,
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(
Font::BOLD,
BLD_TITLE_COLOR,
bg,
BLD_TITLE_COLOR,
BLD_TITLE_COLOR,
)
}
pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, BLD_FG, BLD_BG, BLD_FG, BLD_FG);
pub const TEXT_WARNING: TextStyle = TextStyle::new(
Font::BOLD,
BLD_WARN_COLOR,
BLD_BG,
BLD_WARN_COLOR,
BLD_WARN_COLOR,
);
pub const fn text_fingerprint(bg: Color) -> TextStyle {
TextStyle::new(Font::NORMAL, BLD_FG, bg, BLD_FG, BLD_FG).with_line_breaking(BreakWordsNoHyphen)
}
pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, BLD_FG, BLD_BG, BLD_FG, BLD_FG);
pub const TEXT_WIPE_BOLD: TextStyle = TextStyle::new(
Font::BOLD,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_COLOR,
BLD_WIPE_TEXT_COLOR,
BLD_WIPE_TEXT_COLOR,
);
pub const TEXT_WIPE_NORMAL: TextStyle = TextStyle::new(
Font::NORMAL,
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,66 @@
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, "model_lincoln/res/lock_full.toif");
// Homescreen notifications.
include_icon!(ICON_WARNING40, "model_lincoln/res/warning40.toif");
// TODO: button styles
pub const fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: FG,
button_color: BG,
icon_color: FG,
background_color: BG,
},
active: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: FG,
button_color: BG,
icon_color: FG,
background_color: BG,
},
disabled: &ButtonStyle {
font: Font::DEMIBOLD,
text_color: FG,
button_color: BG,
icon_color: FG,
background_color: BG,
},
}
}
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,495 @@
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::UILincoln;
impl FirmwareUI for UILincoln {
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_blob(
_title: TString<'static>,
_data: Obj,
_description: Option<TString<'static>>,
_text_mono: bool,
_extra: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_verb_info: 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_blob_intro(
_title: TString<'static>,
_data: 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"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>>,
_subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
_verb_info: Option<TString<'static>>,
_verb_cancel: Option<TString<'static>>,
_info_button: bool,
_hold: bool,
_chunkify: bool,
_text_mono: bool,
) -> Result<Gc<LayoutObj>, Error> {
Err::<Gc<LayoutObj>, Error>(Error::ValueError(c"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_mercury(
_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

@ -57,18 +57,18 @@ pub const BUTTON_AREA_START: i16 = HEIGHT - 56;
pub const BUTTON_HEIGHT: i16 = 50;
// BLD icons
pub const X24: &[u8] = include_res!("model_mercury/res/x24.toif");
pub const X32: &[u8] = include_res!("model_mercury/res/x32.toif");
pub const FIRE24: &[u8] = include_res!("model_mercury/res/fire24.toif");
pub const FIRE32: &[u8] = include_res!("model_mercury/res/fire32.toif");
pub const FIRE40: &[u8] = include_res!("model_mercury/res/fire40.toif");
pub const REFRESH24: &[u8] = include_res!("model_mercury/res/refresh24.toif");
pub const MENU32: &[u8] = include_res!("model_mercury/res/menu32.toif");
pub const INFO32: &[u8] = include_res!("model_mercury/res/info32.toif");
pub const DOWNLOAD24: &[u8] = include_res!("model_mercury/res/download24.toif");
pub const WARNING40: &[u8] = include_res!("model_mercury/res/warning40.toif");
pub const CHECK24: &[u8] = include_res!("model_mercury/res/check24.toif");
pub const CHECK40: &[u8] = include_res!("model_mercury/res/check40.toif");
pub const X24: &[u8] = include_res!("model_lincoln/res/bootloader/x24.toif");
pub const X32: &[u8] = include_res!("model_lincoln/res/bootloader/x32.toif");
pub const FIRE24: &[u8] = include_res!("model_lincoln/res/bootloader/fire24.toif");
pub const FIRE32: &[u8] = include_res!("model_lincoln/res/bootloader/fire32.toif");
pub const FIRE40: &[u8] = include_res!("model_lincoln/res/bootloader/fire40.toif");
pub const REFRESH24: &[u8] = include_res!("model_lincoln/res/bootloader/refresh24.toif");
pub const MENU32: &[u8] = include_res!("model_lincoln/res/bootloader/menu32.toif");
pub const INFO32: &[u8] = include_res!("model_lincoln/res/bootloader/info32.toif");
pub const DOWNLOAD24: &[u8] = include_res!("model_lincoln/res/bootloader/download24.toif");
pub const WARNING40: &[u8] = include_res!("model_lincoln/res/bootloader/warning40.toif");
pub const CHECK24: &[u8] = include_res!("model_lincoln/res/bootloader/check24.toif");
pub const CHECK40: &[u8] = include_res!("model_lincoln/res/bootloader/check40.toif");
pub fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {

View File

@ -502,6 +502,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_TR)},
#elif UI_LAYOUT_MERCURY
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_MERCURY)},
#elif UI_LAYOUT_LINCOLN
{MP_ROM_QSTR(MP_QSTR_UI_LAYOUT), MP_ROM_QSTR(MP_QSTR_LINCOLN)},
#else
#error Unknown layout
#endif

View File

@ -31,6 +31,9 @@ def configure_board(
def get_model_ui() -> str:
from SCons.Script import ARGUMENTS
if ARGUMENTS.get('UI_LINCOLN_DEV', '0') == '1':
return "lincoln"
return "tt"

View File

@ -44,11 +44,12 @@ def generate(env):
btc_only = env["bitcoin_only"] == "1"
backlight = env["backlight"]
optiga = env["optiga"]
layout_tt = env["ui_layout"] == "UI_LAYOUT_TT"
layout_tr = env["ui_layout"] == "UI_LAYOUT_TR"
touch = env["use_touch"]
button = env["use_button"]
layout_tt = env["ui_layout"] == "UI_LAYOUT_TT"
layout_tr = env["ui_layout"] == "UI_LAYOUT_TR"
layout_mercury = env["ui_layout"] == "UI_LAYOUT_MERCURY"
layout_lincoln = env["ui_layout"] == "UI_LAYOUT_LINCOLN"
thp = env["thp"]
interim = f"{target[:-4]}.i" # replace .mpy with .i
sed_scripts = [
@ -58,6 +59,7 @@ def generate(env):
rf"-e 's/utils\.UI_LAYOUT == \"TT\"/{layout_tt}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"TR\"/{layout_tr}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"MERCURY\"/{layout_mercury}/g'",
rf"-e 's/utils\.UI_LAYOUT == \"LINCOLN\"/{layout_lincoln}/g'",
rf"-e 's/utils\.USE_BUTTON/{button}/g'",
rf"-e 's/utils\.USE_TOUCH/{touch}/g'",
rf"-e 's/utils\.USE_THP/{thp}/g'",
@ -71,6 +73,7 @@ def generate(env):
"T": "T2T1",
"R": "T2B1",
"T3T1": "T3T1",
"T3W1": "T3W1",
}
for model_sym, internal_model in MODEL_SYMS.items():

View File

@ -2,11 +2,12 @@ from __future__ import annotations
from site_scons import models
from . import mercury, tr, tt
from . import lincoln, mercury, tr, tt
def get_ui_module(model: str):
ui_modules = {
"lincoln": lincoln,
"mercury": mercury,
"tr": tr,
"tt": tt,

View File

@ -0,0 +1,63 @@
from __future__ import annotations
from .common import add_font
def init_ui(
stage: str,
config: list[str],
defines: list[str | tuple[str, str]],
sources: list[str],
rust_features: list[str],
):
rust_features.append("model_lincoln")
font_normal = None
font_demibold = None
font_bold = None
font_mono = None
font_big = None
font_normal_upper = None
font_bold_upper = None
font_sub = None
if stage == "bootloader":
font_normal = "Font_TTSatoshi_DemiBold_21"
font_demibold = "Font_TTSatoshi_DemiBold_21"
font_bold = "Font_TTHoves_Bold_17_upper"
font_mono = "Font_TTSatoshi_DemiBold_21"
font_bold_upper = "Font_TTHoves_Bold_17_upper"
if stage == "prodtest":
font_normal = "Font_TTSatoshi_DemiBold_21"
font_bold = "Font_TTSatoshi_DemiBold_21"
font_mono = "Font_RobotoMono_Medium_21"
if stage == "firmware":
font_normal = "Font_TTSatoshi_DemiBold_21"
font_demibold = "Font_TTSatoshi_DemiBold_21"
font_bold = "Font_TTSatoshi_DemiBold_21"
font_mono = "Font_RobotoMono_Medium_21"
font_big = "Font_TTSatoshi_DemiBold_42"
font_sub = "Font_TTSatoshi_DemiBold_18"
rust_features.append("ui_blurring")
rust_features.append("ui_jpeg_decoder")
rust_features.append("ui_image_buffer")
rust_features.append("ui_overlay")
# fonts
add_font("NORMAL", font_normal, defines, sources)
add_font("BOLD", font_bold, defines, sources)
add_font("DEMIBOLD", font_demibold, defines, sources)
add_font("MONO", font_mono, defines, sources)
add_font("BIG", font_big, defines, sources)
add_font("NORMAL_UPPER", font_normal_upper, defines, sources)
add_font("BOLD_UPPER", font_bold_upper, defines, sources)
add_font("SUB", font_sub, defines, sources)
def get_ui_layout() -> str:
return "UI_LAYOUT_LINCOLN"
def get_ui_layout_path() -> str:
return "trezor/ui/layouts/lincoln/"

View File

@ -173,6 +173,14 @@ trezor.ui.layouts.fido
import trezor.ui.layouts.fido
trezor.ui.layouts.homescreen
import trezor.ui.layouts.homescreen
trezor.ui.layouts.lincoln
import trezor.ui.layouts.lincoln
trezor.ui.layouts.lincoln.fido
import trezor.ui.layouts.lincoln.fido
trezor.ui.layouts.lincoln.recovery
import trezor.ui.layouts.lincoln.recovery
trezor.ui.layouts.lincoln.reset
import trezor.ui.layouts.lincoln.reset
trezor.ui.layouts.mercury
import trezor.ui.layouts.mercury
trezor.ui.layouts.mercury.fido

View File

@ -10,5 +10,7 @@ elif utils.UI_LAYOUT == "TT":
from .tt import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY":
from .mercury import * # noqa: F401,F403
elif utils.UI_LAYOUT == "LINCOLN":
from .lincoln import * # noqa: F401,F403
else:
raise ValueError("Unknown layout")

View File

@ -6,3 +6,5 @@ elif utils.UI_LAYOUT == "TR":
from .tr.fido import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY":
from .mercury.fido import * # noqa: F401,F403
elif utils.UI_LAYOUT == "LINCOLN":
from .lincoln.fido import * # noqa: F401,F403

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 == "TR":
from .tr.recovery import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY":
from .mercury.recovery import * # noqa: F401,F403
elif utils.UI_LAYOUT == "LINCOLN":
from .lincoln.recovery import * # noqa: F401,F403

View File

@ -6,3 +6,5 @@ elif utils.UI_LAYOUT == "TR":
from .tr.reset import * # noqa: F401,F403
elif utils.UI_LAYOUT == "MERCURY":
from .mercury.reset import * # noqa: F401,F403
elif utils.UI_LAYOUT == "LINCOLN":
from .lincoln.reset import * # noqa: F401,F403

View File

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

View File

@ -79,6 +79,7 @@ class LayoutType(Enum):
TT = auto()
TR = auto()
Mercury = auto()
Lincoln = auto()
@classmethod
def from_model(cls, model: models.TrezorModel) -> "LayoutType":
@ -90,6 +91,8 @@ class LayoutType(Enum):
return cls.Mercury
if model in (models.T1B1,):
return cls.T1
if model in (models.T3W1,):
return cls.Lincoln
raise ValueError(f"Unknown model: {model}")

View File

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

View File

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

View File

@ -12,6 +12,7 @@ FIRMWARE_LENGTHS = {
models.T2B1: 13 * 128 * 1024,
models.T3T1: 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.T3T1: 256 * 1024,
models.T3B1: 256 * 1024,
models.T3W1: 256 * 1024, # FIXME: fill in correct value
}