feat(core): copy T2T1 bootloader UI for T3T1

pull/3626/head
Martin Milata 4 weeks ago
parent b5fa5a3f3a
commit cf00726152

@ -118,7 +118,7 @@ jobs:
submodules: recursive
- uses: ./.github/actions/environment
- run: nix-shell --run "poetry run make -C core build_bootloader_emu"
if: matrix.coins == 'universal' && matrix.model != 'T3T1' # FIXME T3T1 bootloader emulator
if: matrix.coins == 'universal'
- run: nix-shell --run "poetry run make -C core build_unix_frozen"
- run: cp core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-${{ matrix.model }}-${{ matrix.coins }}
- uses: actions/upload-artifact@v4

@ -220,6 +220,8 @@ def cargo_build():
features = ["model_t1"]
elif TREZOR_MODEL in ("R",):
features = ["model_tr"]
elif TREZOR_MODEL in ("T3T1",):
features = ["model_mercury"]
else:
features = ["model_tt"]
features.append("ui")

@ -274,6 +274,8 @@ def cargo_build():
features = ["model_t1"]
elif TREZOR_MODEL in ("R",):
features = ["model_tr"]
elif TREZOR_MODEL in ("T3T1",):
features = ["model_mercury"]
else:
features = ["model_tt"]

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

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

@ -5,16 +5,16 @@ mod starry;
use crate::ui::display::{Color, Icon};
#[cfg(feature = "model_tt")]
#[cfg(any(feature = "model_tt", feature = "model_mercury"))]
use crate::ui::display::loader::circular::{
loader_circular as determinate, loader_circular_indeterminate as indeterminate,
};
#[cfg(feature = "model_tt")]
#[cfg(any(feature = "model_tt", feature = "model_mercury"))]
pub use crate::ui::display::loader::circular::{loader_circular_uncompress, LoaderDimensions};
#[cfg(not(feature = "model_tt"))]
#[cfg(all(not(feature = "model_tt"), not(feature = "model_mercury")))]
use crate::ui::display::loader::rectangular::loader_rectangular as determinate;
#[cfg(not(feature = "model_tt"))]
#[cfg(all(not(feature = "model_tt"), not(feature = "model_mercury")))]
use crate::ui::display::loader::starry::loader_starry_indeterminate as indeterminate;
pub use small::loader_small_indeterminate;

@ -14,6 +14,9 @@ pub mod util;
pub mod layout;
mod api;
#[cfg(feature = "model_mercury")]
pub mod model_mercury;
#[cfg(feature = "model_tr")]
pub mod model_tr;
#[cfg(feature = "model_tt")]

@ -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},
},
};
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())),
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 paint(&mut self) {
self.bg.paint();
self.title.paint();
self.text.paint();
self.warn.paint();
self.host.paint();
self.menu.paint();
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.menu.bounds(sink);
}
}

@ -0,0 +1,117 @@
use crate::{
trezorhal::secbool::{secbool, sectrue},
ui::{
component::{Child, Component, Event, EventCtx, Label, Pad},
constant::{screen, WIDTH},
display::Icon,
geometry::{Insets, Point, Rect},
},
};
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 paint(&mut self) {
self.bg.paint();
self.title.paint();
self.close.paint();
self.reboot.paint();
self.reset.paint();
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.close.bounds(sink);
self.reboot.bounds(sink);
self.reset.bounds(sink);
}
}

@ -0,0 +1,320 @@
use heapless::String;
use crate::{
trezorhal::secbool::secbool,
ui::{
component::{connect::Connect, Label},
display::{self, Color, Font, Icon},
geometry::{Point, Rect},
layout::simplified::{run, show},
},
};
use super::{
bootloader::welcome::Welcome,
component::{
bl_confirm::{Confirm, ConfirmTitle},
Button, ResultScreen, WelcomeScreen,
},
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, DOWNLOAD32, FIRE32,
FIRE40, RESULT_FW_INSTALL, RESULT_INITIAL, RESULT_WIPE, TEXT_BOLD, TEXT_NORMAL,
TEXT_WIPE_BOLD, TEXT_WIPE_NORMAL, WARNING40, WELCOME_COLOR, X24,
},
BACKLIGHT_NORMAL, BLACK, FG, WHITE,
},
ModelMercuryFeatures,
};
use crate::ui::{ui_features::UIFeaturesBootloader, UIFeaturesCommon};
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 = ModelMercuryFeatures::SCREEN;
impl ModelMercuryFeatures {
fn screen_progress(
text: &str,
progress: u16,
initialize: bool,
fg_color: Color,
bg_color: Color,
icon: Option<(Icon, Color)>,
) {
if initialize {
ModelMercuryFeatures::fadeout();
display::rect_fill(SCREEN, bg_color);
}
display::text_center(
Point::new(SCREEN.width() / 2, SCREEN.height() - 45),
text,
Font::NORMAL,
fg_color,
bg_color,
);
display::loader(progress, -20, fg_color, bg_color, icon);
display::refresh();
if initialize {
ModelMercuryFeatures::fadein();
}
}
fn screen_install_success_bld(msg: &str, complete_draw: bool) {
let mut frame = ResultScreen::new(
&RESULT_FW_INSTALL,
Icon::new(CHECK40),
"Firmware installed\nsuccessfully".into(),
Label::centered(msg.into(), RESULT_FW_INSTALL.title_style()).vertically_centered(),
complete_draw,
);
show(&mut frame, complete_draw);
}
fn screen_install_success_initial(msg: &str, complete_draw: bool) {
let mut frame = ResultScreen::new(
&RESULT_INITIAL,
Icon::new(CHECK40),
"Firmware installed\nsuccessfully".into(),
Label::centered(msg.into(), RESULT_INITIAL.title_style()).vertically_centered(),
complete_draw,
);
show(&mut frame, complete_draw);
}
}
impl UIFeaturesBootloader for ModelMercuryFeatures {
fn screen_welcome() {
let mut frame = Welcome::new();
show(&mut frame, true);
}
fn bld_continue_label(bg_color: Color) {
display::text_center(
Point::new(SCREEN.width() / 2, SCREEN.height() - 5),
"click to continue ...",
Font::NORMAL,
WHITE,
bg_color,
);
}
fn screen_install_success(restart_seconds: u8, initial_setup: bool, complete_draw: bool) {
let mut reboot_msg = BootloaderString::new();
if restart_seconds >= 1 {
unwrap!(reboot_msg.push_str("RESTARTING IN "));
// 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));
} else {
unwrap!(reboot_msg.push_str(RECONNECT_MESSAGE));
}
if initial_setup {
ModelMercuryFeatures::screen_install_success_initial(reboot_msg.as_str(), complete_draw)
} else {
ModelMercuryFeatures::screen_install_success_bld(reboot_msg.as_str(), complete_draw)
}
display::refresh();
}
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,
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_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());
let r = Button::with_text("INSTALL".into()).styled(button_confirm());
(l, r)
} else {
let l = Button::with_icon(Icon::new(X24)).styled(button_bld());
let r = Button::with_icon(Icon::new(CHECK24)).styled(button_confirm());
(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());
let left = Button::with_text("CANCEL".into()).styled(button_wipe_cancel());
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 {
unimplemented!();
}
fn screen_unlock_bootloader_success() {
unimplemented!();
}
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_empty(fading: bool) {
if fading {
ModelMercuryFeatures::fadeout();
}
display::rect_fill(SCREEN, BLACK);
let mut frame = WelcomeScreen::new(true);
show(&mut frame, false);
if fading {
ModelMercuryFeatures::fadein();
} else {
display::set_backlight(BACKLIGHT_NORMAL);
}
display::refresh();
}
fn screen_wipe_progress(progress: u16, initialize: bool) {
ModelMercuryFeatures::screen_progress(
"Resetting Trezor",
progress,
initialize,
BLD_FG,
BLD_WIPE_COLOR,
Some((Icon::new(FIRE32), BLD_FG)),
)
}
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 { FG } else { BLD_FG };
ModelMercuryFeatures::screen_progress(
"Installing firmware",
progress,
initialize,
fg_color,
bg_color,
Some((Icon::new(DOWNLOAD32), fg_color)),
)
}
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);
}
}

@ -0,0 +1,60 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never, Pad},
constant::screen,
display::{self, Font, Icon},
geometry::{Alignment2D, Offset, Rect},
};
use super::super::theme::{
bootloader::{START_URL, WELCOME_COLOR},
BLACK, GREY_MEDIUM, WHITE,
};
pub struct Welcome {
bg: Pad,
}
impl Welcome {
pub fn new() -> Self {
Self {
bg: Pad::with_background(WELCOME_COLOR).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 paint(&mut self) {
self.bg.paint();
display::text_center(
screen().top_center() + Offset::y(102),
"Get started with",
Font::NORMAL,
GREY_MEDIUM,
BLACK,
);
display::text_center(
screen().top_center() + Offset::y(126),
"your Trezor at",
Font::NORMAL,
GREY_MEDIUM,
BLACK,
);
Icon::new(START_URL).draw(
screen().top_center() + Offset::y(135),
Alignment2D::TOP_CENTER,
WHITE,
BLACK,
);
}
}

@ -0,0 +1,254 @@
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},
},
};
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 paint(&mut self) {
self.bg.paint();
self.content_pad.paint();
if let Some(info) = self.info.as_mut() {
if self.show_info {
info.close_button.paint();
info.title.paint();
info.text.paint();
self.left_button.paint();
self.right_button.paint();
// short-circuit before painting the main components
return;
} else {
info.info_button.paint();
// pass through to the rest of the paint
}
}
self.message.paint();
self.alert.paint();
self.left_button.paint();
self.right_button.paint();
match &mut self.title {
ConfirmTitle::Text(label) => label.paint(),
ConfirmTitle::Icon(icon) => {
icon.draw(
Point::new(screen().center().x, ICON_TOP),
Alignment2D::TOP_CENTER,
WHITE,
self.bg_color,
);
}
}
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.left_button.bounds(sink);
self.right_button.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Confirm<'_> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("BlConfirm");
}
}

@ -0,0 +1,440 @@
#[cfg(feature = "haptic")]
use crate::trezorhal::haptic::{play, HapticEffect};
use crate::{
strutil::TString,
time::Duration,
ui::{
component::{Component, Event, EventCtx, TimerToken},
display::{self, toif::Icon, Color, Font},
event::TouchEvent,
geometry::{Alignment2D, Insets, Offset, Point, Rect},
},
};
use super::theme;
pub enum ButtonMsg {
Pressed,
Released,
Clicked,
LongPressed,
}
pub struct Button {
area: Rect,
touch_expand: Option<Insets>,
content: ButtonContent,
styles: ButtonStyleSheet,
state: State,
long_press: Option<Duration>,
long_timer: Option<TimerToken>,
}
impl Button {
/// Offsets the baseline of the button text either up (negative) or down
/// (positive).
pub const BASELINE_OFFSET: i16 = -2;
pub const fn new(content: ButtonContent) -> Self {
Self {
content,
area: Rect::zero(),
touch_expand: None,
styles: theme::button_default(),
state: State::Initial,
long_press: None,
long_timer: None,
}
}
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 with_icon_blend(bg: Icon, fg: Icon, fg_offset: Offset) -> Self {
Self::new(ButtonContent::IconBlend(bg, fg, fg_offset))
}
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_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 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, ctx: &mut EventCtx, content: ButtonContent) {
if self.content != content {
self.content = content;
ctx.request_paint();
}
}
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 paint_background(&self, style: &ButtonStyle) {
match &self.content {
ButtonContent::IconBlend(_, _, _) => {}
_ => {
if style.border_width > 0 {
// Paint the border and a smaller background on top of it.
display::rect_fill_rounded(
self.area,
style.border_color,
style.background_color,
style.border_radius,
);
display::rect_fill_rounded(
self.area.inset(Insets::uniform(style.border_width)),
style.button_color,
style.border_color,
style.border_radius,
);
} else {
// We do not need to draw an explicit border in this case, just a
// bigger background.
display::rect_fill_rounded(
self.area,
style.button_color,
style.background_color,
style.border_radius,
);
}
}
}
}
pub fn paint_content(&self, style: &ButtonStyle) {
match &self.content {
ButtonContent::Empty => {}
ButtonContent::Text(text) => {
let width = text.map(|c| style.font.text_width(c));
let height = style.font.text_height();
let start_of_baseline = self.area.center()
+ Offset::new(-width / 2, height / 2)
+ Offset::y(Self::BASELINE_OFFSET);
text.map(|text| {
display::text_left(
start_of_baseline,
text,
style.font,
style.text_color,
style.button_color,
);
});
}
ButtonContent::Icon(icon) => {
icon.draw(
self.area.center(),
Alignment2D::CENTER,
style.text_color,
style.button_color,
);
}
ButtonContent::IconAndText(child) => {
child.paint(self.area, self.style(), Self::BASELINE_OFFSET);
}
ButtonContent::IconBlend(bg, fg, offset) => display::icon_over_icon(
Some(self.area),
(*bg, Offset::zero(), style.button_color),
(*fg, *offset, style.text_color),
style.background_color,
),
}
}
}
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")]
play(HapticEffect::ButtonPress);
self.set(ctx, State::Pressed);
if let Some(duration) = self.long_press {
self.long_timer = Some(ctx.request_timer(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);
}
_ => {
// Touch finished outside our area.
self.set(ctx, State::Initial);
self.long_timer = None;
}
}
}
Event::Timer(token) => {
if self.long_timer == Some(token) {
self.long_timer = None;
if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")]
play(HapticEffect::ButtonPress);
self.set(ctx, State::Initial);
return Some(ButtonMsg::LongPressed);
}
}
}
_ => {}
};
None
}
fn paint(&mut self) {
let style = self.style();
self.paint_background(style);
self.paint_content(style);
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area);
}
}
#[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.into());
t.bool("icon", true);
}
ButtonContent::IconBlend(_, _, _) => t.bool("icon", true),
}
}
}
#[derive(PartialEq, Eq)]
enum State {
Initial,
Pressed,
Released,
Disabled,
}
#[derive(PartialEq, Eq)]
pub enum ButtonContent {
Empty,
Text(TString<'static>),
Icon(Icon),
IconAndText(IconText),
IconBlend(Icon, Icon, Offset),
}
#[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 background_color: Color,
pub border_color: Color,
pub border_radius: u8,
pub border_width: i16,
}
#[derive(Clone, Copy)]
pub enum CancelInfoConfirmMsg {
Cancelled,
Info,
Confirmed,
}
#[derive(PartialEq, Eq)]
pub struct IconText {
text: &'static str,
icon: Icon,
}
impl IconText {
const ICON_SPACE: i16 = 46;
const ICON_MARGIN: i16 = 4;
const TEXT_MARGIN: i16 = 6;
pub fn new(text: &'static str, icon: Icon) -> Self {
Self { text, icon }
}
pub fn paint(&self, area: Rect, style: &ButtonStyle, baseline_offset: i16) {
let width = style.font.text_width(self.text);
let height = style.font.text_height();
let mut use_icon = false;
let mut use_text = false;
let mut icon_pos = Point::new(
area.top_left().x + ((Self::ICON_SPACE + Self::ICON_MARGIN) / 2),
area.center().y,
);
let mut text_pos =
area.center() + Offset::new(-width / 2, height / 2) + Offset::y(baseline_offset);
if area.width() > (Self::ICON_SPACE + Self::TEXT_MARGIN + width) {
//display both icon and text
text_pos = Point::new(area.top_left().x + Self::ICON_SPACE, text_pos.y);
use_text = true;
use_icon = true;
} else if area.width() > (width + Self::TEXT_MARGIN) {
use_text = true;
} else {
//if we can't fit the text, retreat to centering the icon
icon_pos = area.center();
use_icon = true;
}
if use_text {
display::text_left(
text_pos,
self.text,
style.font,
style.text_color,
style.button_color,
);
}
if use_icon {
self.icon.draw(
icon_pos,
Alignment2D::CENTER,
style.text_color,
style.button_color,
);
}
}
}

@ -0,0 +1,92 @@
use crate::{
strutil::TString,
ui::{
component::{Child, Component, Event, EventCtx, Label, Never, Pad},
constant::screen,
geometry::{Alignment2D, Point, Rect},
},
};
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 = 116;
#[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: Child<Label<'a>>,
message: Child<Label<'a>>,
footer: Child<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());
let footer = ResultFooter::new(
Label::centered(footer, STYLE.title_style()).vertically_centered(),
STYLE,
);
Self {
bg: Pad::with_background(FATAL_ERROR_COLOR).with_clear(),
title: Child::new(title),
message: Child::new(message),
footer: Child::new(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 paint(&mut self) {
self.bg.paint();
let icon = ICON_WARNING40;
icon.draw(
Point::new(screen().center().x, ICON_TOP),
Alignment2D::TOP_CENTER,
WHITE,
FATAL_ERROR_COLOR,
);
self.title.paint();
self.message.paint();
self.footer.paint();
}
}

@ -0,0 +1,204 @@
use super::theme;
use crate::{
strutil::TString,
ui::{
component::{
base::ComponentExt, label::Label, text::TextStyle, Child, Component, Event, EventCtx,
},
display::Icon,
geometry::{Alignment, Insets, Offset, Rect},
},
};
use super::{Button, ButtonMsg, CancelInfoConfirmMsg};
pub struct Frame<T> {
border: Insets,
title: Child<Label<'static>>,
subtitle: Option<Child<Label<'static>>>,
button: Option<Child<Button>>,
button_msg: CancelInfoConfirmMsg,
content: Child<T>,
}
pub enum FrameMsg<T> {
Content(T),
Button(CancelInfoConfirmMsg),
}
impl<T> Frame<T>
where
T: Component,
{
pub fn new(
style: TextStyle,
alignment: Alignment,
title: TString<'static>,
content: T,
) -> Self {
Self {
title: Child::new(Label::new(title, alignment, style)),
subtitle: None,
border: theme::borders(),
button: None,
button_msg: CancelInfoConfirmMsg::Cancelled,
content: Child::new(content),
}
}
pub fn left_aligned(style: TextStyle, title: TString<'static>, content: T) -> Self {
Self::new(style, Alignment::Start, title, content)
}
pub fn right_aligned(style: TextStyle, title: TString<'static>, content: T) -> Self {
Self::new(style, Alignment::End, title, content)
}
pub fn centered(style: TextStyle, title: TString<'static>, content: T) -> Self {
Self::new(style, Alignment::Center, title, content)
}
pub fn with_border(mut self, border: Insets) -> Self {
self.border = border;
self
}
pub fn with_subtitle(mut self, style: TextStyle, subtitle: TString<'static>) -> Self {
self.subtitle = Some(Child::new(Label::new(
subtitle,
self.title.inner().alignment(),
style,
)));
self
}
fn with_button(mut self, icon: Icon, msg: CancelInfoConfirmMsg) -> Self {
let touch_area = Insets {
left: self.border.left * 4,
bottom: self.border.bottom * 4,
..self.border
};
self.button = Some(Child::new(
Button::with_icon(icon)
.with_expanded_touch_area(touch_area)
.styled(theme::button_moreinfo()),
));
self.button_msg = msg;
self
}
pub fn with_cancel_button(self) -> Self {
self.with_button(theme::ICON_CORNER_CANCEL, CancelInfoConfirmMsg::Cancelled)
}
pub fn with_info_button(self) -> Self {
self.with_button(theme::ICON_CORNER_INFO, CancelInfoConfirmMsg::Info)
}
pub fn inner(&self) -> &T {
self.content.inner()
}
pub fn update_title(&mut self, ctx: &mut EventCtx, new_title: TString<'static>) {
self.title.mutate(ctx, |ctx, t| {
t.set_text(new_title);
t.request_complete_repaint(ctx)
})
}
pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R
where
F: Fn(&mut T) -> R,
{
self.content.mutate(ctx, |ctx, c| {
let res = update_fn(c);
c.request_complete_repaint(ctx);
res
})
}
}
impl<T> Component for Frame<T>
where
T: Component,
{
type Msg = FrameMsg<T::Msg>;
fn place(&mut self, bounds: Rect) -> Rect {
const TITLE_SPACE: i16 = theme::BUTTON_SPACING;
let bounds = bounds.inset(self.border);
// Allowing for little longer titles to fit in
const TITLE_EXTRA_SPACE: Insets = Insets::right(2);
if let Some(b) = &mut self.button {
let button_side = theme::CORNER_BUTTON_SIDE;
let (header_area, button_area) = bounds.split_right(button_side);
let (button_area, _) = button_area.split_top(button_side);
b.place(button_area);
let title_area = self.title.place(header_area.outset(TITLE_EXTRA_SPACE));
let remaining = header_area.inset(Insets::top(title_area.height()));
let subtitle_area = self.subtitle.place(remaining);
let title_height = title_area.height() + subtitle_area.height();
let header_height = title_height.max(button_side);
if title_height < button_side {
self.title
.place(title_area.translate(Offset::y((button_side - title_height) / 2)));
self.subtitle
.place(subtitle_area.translate(Offset::y((button_side - title_height) / 2)));
}
let content_area = bounds.inset(Insets::top(header_height + TITLE_SPACE));
self.content.place(content_area);
} else {
let title_area = self.title.place(bounds.outset(TITLE_EXTRA_SPACE));
let remaining = bounds.inset(Insets::top(title_area.height()));
let subtitle_area = self.subtitle.place(remaining);
let remaining = remaining.inset(Insets::top(subtitle_area.height()));
let content_area = remaining.inset(Insets::top(TITLE_SPACE));
self.content.place(content_area);
}
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
self.subtitle.event(ctx, event);
if let Some(ButtonMsg::Clicked) = self.button.event(ctx, event) {
return Some(FrameMsg::Button(self.button_msg));
}
self.content.event(ctx, event).map(FrameMsg::Content)
}
fn paint(&mut self) {
self.title.paint();
self.subtitle.paint();
self.button.paint();
self.content.paint();
}
#[cfg(feature = "ui_bounds")]
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.title.bounds(sink);
self.subtitle.bounds(sink);
self.button.bounds(sink);
self.content.bounds(sink);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Frame<T>
where
T: crate::trace::Trace,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Frame");
t.child("title", &self.title);
t.child("content", &self.content);
if let Some(subtitle) = &self.subtitle {
t.child("subtitle", subtitle);
}
if let Some(button) = &self.button {
t.child("button", button);
}
}
}

@ -0,0 +1,253 @@
#[cfg(feature = "haptic")]
use crate::trezorhal::haptic::{play, HapticEffect};
use crate::{
time::{Duration, Instant},
ui::{
animation::Animation,
component::{Component, Event, EventCtx, Pad},
display::{self, toif::Icon, Color},
geometry::{Offset, Rect},
util::animation_disabled,
},
};
use super::{constant, theme};
const GROWING_DURATION_MS: u32 = 1000;
const SHRINKING_DURATION_MS: u32 = 500;
pub enum LoaderMsg {
GrownCompletely,
ShrunkCompletely,
}
enum State {
Initial,
Growing(Animation<u16>),
Shrinking(Animation<u16>),
}
pub struct Loader {
pub pad: Pad,
state: State,
growing_duration: Duration,
shrinking_duration: Duration,
styles: LoaderStyleSheet,
offset_y: i16,
}
impl Loader {
pub const SIZE: Offset = Offset::new(120, 120);
pub fn new() -> Self {
let styles = theme::loader_default();
Self::with_styles(styles)
}
pub fn with_lock_icon() -> Self {
let styles = theme::loader_lock_icon();
Self::with_styles(styles)
}
pub fn with_styles(styles: LoaderStyleSheet) -> Self {
Self {
pad: Pad::with_background(styles.normal.background_color),
state: State::Initial,
growing_duration: Duration::from_millis(GROWING_DURATION_MS),
shrinking_duration: Duration::from_millis(SHRINKING_DURATION_MS),
styles,
offset_y: 0,
}
}
pub fn with_durations(
mut self,
growing_duration: Duration,
shrinking_duration: Duration,
) -> Self {
self.growing_duration = growing_duration;
self.shrinking_duration = shrinking_duration;
self
}
pub fn start_growing(&mut self, ctx: &mut EventCtx, now: Instant) {
let mut anim = Animation::new(
display::LOADER_MIN,
display::LOADER_MAX,
self.growing_duration,
now,
);
if let State::Shrinking(shrinking) = &self.state {
anim.seek_to_value(shrinking.value(now));
}
self.state = State::Growing(anim);
// The animation is starting, request an animation frame event.
ctx.request_anim_frame();
// We don't have to wait for the animation frame event with the first paint,
// let's do that now.
ctx.request_paint();
}
pub fn start_shrinking(&mut self, ctx: &mut EventCtx, now: Instant) {
let mut anim = Animation::new(
display::LOADER_MAX,
display::LOADER_MIN,
self.shrinking_duration,
now,
);
if let State::Growing(growing) = &self.state {
anim.seek_to_value(display::LOADER_MAX.saturating_sub(growing.value(now)));
}
self.state = State::Shrinking(anim);
// Request anim frame as the animation may not be running, e.g. when already
// grown completely.
ctx.request_anim_frame();
// We don't have to wait for the animation frame event with next paint,
// let's do that now.
ctx.request_paint();
}
pub fn reset(&mut self) {
self.state = State::Initial;
}
pub fn animation(&self) -> Option<&Animation<u16>> {
match &self.state {
State::Initial => None,
State::Growing(a) | State::Shrinking(a) => Some(a),
}
}
pub fn progress(&self, now: Instant) -> Option<u16> {
self.animation().map(|a| a.value(now))
}
pub fn is_animating(&self) -> bool {
self.animation().is_some()
}
pub fn is_completely_grown(&self, now: Instant) -> bool {
matches!(self.progress(now), Some(display::LOADER_MAX))
}
pub fn is_completely_shrunk(&self, now: Instant) -> bool {
matches!(self.progress(now), Some(display::LOADER_MIN))
}
}
impl Component for Loader {
type Msg = LoaderMsg;
fn place(&mut self, bounds: Rect) -> Rect {
// Current loader API only takes Y-offset relative to screen center, which we
// compute from the bounds center point.
let screen_center = constant::screen().center();
self.offset_y = bounds.center().y - screen_center.y;
// FIXME: avoid umlauts rendering outside bounds
let mut bounds_up_to_top = bounds;
bounds_up_to_top.y0 = 0;
self.pad.place(bounds_up_to_top);
Rect::from_center_and_size(bounds.center(), Self::SIZE)
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let now = Instant::now();
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.is_animating() {
// We have something to paint, so request to be painted in the next pass.
if !animation_disabled() {
ctx.request_paint();
}
if self.is_completely_grown(now) {
#[cfg(feature = "haptic")]
play(HapticEffect::HoldToConfirm);
return Some(LoaderMsg::GrownCompletely);
} else if self.is_completely_shrunk(now) {
return Some(LoaderMsg::ShrunkCompletely);
} else {
// There is further progress in the animation, request an animation frame event.
ctx.request_anim_frame();
}
}
}
None
}
fn paint(&mut self) {
// TODO: Consider passing the current instant along with the event -- that way,
// we could synchronize painting across the component tree. Also could be useful
// in automated tests.
// In practice, taking the current instant here is more precise in case some
// other component in the tree takes a long time to draw.
let now = Instant::now();
if let Some(progress) = self.progress(now) {
let style = if progress < display::LOADER_MAX {
self.styles.normal
} else {
self.styles.active
};
self.pad.paint();
display::loader(
progress,
self.offset_y,
style.loader_color,
style.background_color,
style.icon,
);
}
}
}
pub struct LoaderStyleSheet {
pub normal: &'static LoaderStyle,
pub active: &'static LoaderStyle,
}
pub struct LoaderStyle {
pub icon: Option<(Icon, Color)>,
pub loader_color: Color,
pub background_color: Color,
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for Loader {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("Loader");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loader_yields_expected_progress() {
let mut ctx = EventCtx::new();
let mut l = Loader::new();
let t = Instant::now();
assert_eq!(l.progress(t), None);
l.start_growing(&mut ctx, t);
assert_eq!(l.progress(t), Some(0));
let t = add_millis(t, 500);
assert_eq!(l.progress(t), Some(500));
l.start_shrinking(&mut ctx, t);
assert_eq!(l.progress(t), Some(500));
let t = add_millis(t, 125);
assert_eq!(l.progress(t), Some(250));
let t = add_millis(t, 125);
assert_eq!(l.progress(t), Some(0));
}
fn add_millis(inst: Instant, millis: u32) -> Instant {
inst.checked_add(Duration::from_millis(millis)).unwrap()
}
}

@ -0,0 +1,18 @@
pub mod bl_confirm;
mod button;
mod error;
mod frame;
mod loader;
mod result;
mod welcome_screen;
pub use button::{
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, CancelInfoConfirmMsg, IconText,
};
pub use error::ErrorScreen;
pub use frame::{Frame, FrameMsg};
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use welcome_screen::WelcomeScreen;
use super::{constant, theme};

@ -0,0 +1,168 @@
use crate::{
strutil::TString,
ui::{
component::{text::TextStyle, Child, Component, Event, EventCtx, Label, Never, Pad},
constant::screen,
display::{self, Color, Font, Icon},
geometry::{Alignment2D, Insets, Offset, Point, Rect},
},
};
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 paint(&mut self) {
// divider line
let bar = Rect::from_center_and_size(
Point::new(self.area.center().x, self.area.y0),
Offset::new(self.area.width(), 1),
);
display::rect_fill(bar, self.style.divider_color);
// footer text
self.text.paint();
}
}
pub struct ResultScreen<'a> {
bg: Pad,
footer_pad: Pad,
style: &'a ResultStyle,
icon: Icon,
message: Child<Label<'a>>,
footer: Child<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: Child::new(Label::centered(message, style.message_style())),
footer: Child::new(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 paint(&mut self) {
self.bg.paint();
self.footer_pad.paint();
self.icon.draw(
Point::new(screen().center().x, ICON_CENTER_Y),
Alignment2D::CENTER,
self.style.fg_color,
self.style.bg_color,
);
self.message.paint();
self.footer.paint();
}
}

@ -0,0 +1,81 @@
use crate::ui::{
component::{Component, Event, EventCtx, Never},
geometry::{Alignment2D, Offset, Rect},
};
use super::theme;
#[cfg(feature = "bootloader")]
use crate::ui::{display::Icon, model_mercury::theme::bootloader::DEVICE_NAME};
const TEXT_BOTTOM_MARGIN: i16 = 24; // matching the homescreen label margin
const ICON_TOP_MARGIN: i16 = 48;
#[cfg(not(feature = "bootloader"))]
const MODEL_NAME_FONT: display::Font = display::Font::DEMIBOLD;
#[cfg(not(feature = "bootloader"))]
use crate::{trezorhal::model, ui::display};
pub struct WelcomeScreen {
area: Rect,
empty_lock: bool,
}
impl WelcomeScreen {
pub fn new(empty_lock: bool) -> Self {
Self {
area: Rect::zero(),
empty_lock,
}
}
}
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 paint(&mut self) {
let logo = if self.empty_lock {
theme::ICON_LOGO_EMPTY
} else {
theme::ICON_LOGO
};
logo.draw(
self.area.top_center() + Offset::y(ICON_TOP_MARGIN),
Alignment2D::TOP_CENTER,
theme::FG,
theme::BG,
);
#[cfg(not(feature = "bootloader"))]
display::text_center(
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
model::FULL_NAME,
MODEL_NAME_FONT,
theme::FG,
theme::BG,
);
#[cfg(feature = "bootloader")]
Icon::new(DEVICE_NAME).draw(
self.area.bottom_center() - Offset::y(TEXT_BOTTOM_MARGIN),
Alignment2D::BOTTOM_CENTER,
theme::FG,
theme::BG,
);
}
}
#[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());
}
}

@ -0,0 +1,22 @@
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 _;
pub const LINE_SPACE: i16 = 4;
pub const FONT_BPP: i16 = 4;
pub const LOADER_OUTER: i16 = 60;
pub const LOADER_INNER: i16 = 42;
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();

@ -0,0 +1,33 @@
use super::{geometry::Rect, UIFeaturesCommon};
#[cfg(feature = "bootloader")]
pub mod bootloader;
pub mod component;
pub mod constant;
pub mod theme;
mod screens;
pub struct ModelMercuryFeatures;
impl UIFeaturesCommon for ModelMercuryFeatures {
fn fadein() {
#[cfg(feature = "backlight")]
crate::ui::display::fade_backlight_duration(theme::BACKLIGHT_NORMAL, 150);
}
fn fadeout() {
#[cfg(feature = "backlight")]
crate::ui::display::fade_backlight_duration(theme::BACKLIGHT_DIM, 150);
}
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_full() {
screens::screen_boot_full();
}
}

@ -0,0 +1,20 @@
use crate::ui::{component::Component, constant::screen, display};
use super::{
component::{ErrorScreen, WelcomeScreen},
constant,
};
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());
frame.paint();
}
pub fn screen_boot_full() {
let mut frame = WelcomeScreen::new(false);
frame.place(screen());
display::sync();
frame.paint();
display::refresh();
}

@ -0,0 +1,281 @@
use crate::ui::{
component::{text::TextStyle, LineBreaking::BreakWordsNoHyphen},
constant::{HEIGHT, WIDTH},
display::{Color, Font},
geometry::{Offset, Point, Rect},
};
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 DOWNLOAD32: &[u8] = include_res!("model_mercury/res/download32.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 DEVICE_NAME: &[u8] = include_res!("model_mercury/res/device_name_T.toif");
pub const START_URL: &[u8] = include_res!("model_mercury/res/start.toif");
pub fn button_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_BG,
button_color: WHITE,
background_color: BLD_BG,
border_color: BLD_BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_BG,
button_color: BLD_INSTALL_BTN_COLOR_ACTIVE,
background_color: BLD_BG,
border_color: BLD_BG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: GREY_DARK,
background_color: FG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub fn button_wipe_cancel() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: WHITE,
button_color: BLD_WIPE_CANCEL_BTN_COLOR,
background_color: BLD_WIPE_COLOR,
border_color: BLD_WIPE_COLOR,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: WHITE,
button_color: BLD_WIPE_CANCEL_BTN_COLOR_ACTIVE,
background_color: BLD_WIPE_COLOR,
border_color: BLD_WIPE_COLOR,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: GREY_DARK,
background_color: WHITE,
border_color: WHITE,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub fn button_wipe_confirm() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_WIPE_COLOR,
button_color: BLD_WIPE_BTN_COLOR,
background_color: BLD_WIPE_COLOR,
border_color: BLD_WIPE_COLOR,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_WIPE_COLOR,
button_color: BLD_WIPE_BTN_COLOR_ACTIVE,
background_color: BLD_WIPE_COLOR,
border_color: BLD_WIPE_COLOR,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: GREY_DARK,
background_color: FG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub fn button_bld_menu() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BG,
background_color: BLD_BG,
border_color: BLD_BTN_COLOR,
border_radius: 2,
border_width: 2,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BG,
background_color: BLD_BG,
border_color: BLD_BTN_COLOR_ACTIVE,
border_radius: 2,
border_width: 2,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: BLD_BG,
background_color: BLD_BG,
border_color: BLD_BG,
border_radius: 2,
border_width: 2,
},
}
}
pub fn button_bld() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BTN_COLOR,
background_color: BLD_BG,
border_color: BLD_BG,
border_radius: 4,
border_width: 0,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: BLD_FG,
button_color: BLD_BTN_COLOR_ACTIVE,
background_color: BLD_BG,
border_color: BLD_BG,
border_radius: 4,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: BLD_BTN_COLOR,
background_color: BLD_BG,
border_color: BLD_BG,
border_radius: 4,
border_width: 0,
},
}
}
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);

@ -0,0 +1,190 @@
pub mod bootloader;
use crate::ui::{
component::text::{LineBreaking, PageBreaking, TextStyle},
display::{Color, Font, Icon},
geometry::Insets,
};
use super::component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet};
// Typical backlight values.
pub const BACKLIGHT_NORMAL: u16 = 150;
pub const BACKLIGHT_DIM: u16 = 5;
// Color palette.
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 RED: Color = Color::rgb(0xE7, 0x0E, 0x0E); // button
pub const RED_DARK: Color = Color::rgb(0xAE, 0x09, 0x09); // button pressed
pub const YELLOW: Color = Color::rgb(0xD9, 0x9E, 0x00); // button
pub const YELLOW_DARK: Color = Color::rgb(0x7A, 0x58, 0x00); // button pressed
pub const GREEN: Color = Color::rgb(0x00, 0xAA, 0x35); // button
pub const GREEN_DARK: Color = Color::rgb(0x00, 0x55, 0x1D); // button pressed
pub const BLUE: Color = Color::rgb(0x06, 0x1E, 0xAD); // button
pub const BLUE_DARK: Color = Color::rgb(0x04, 0x10, 0x58); // button pressed
pub const OFF_WHITE: Color = Color::rgb(0xDE, 0xDE, 0xDE); // very light grey
pub const GREY_LIGHT: Color = Color::rgb(0x90, 0x90, 0x90); // secondary text
pub const GREY_MEDIUM: Color = Color::rgb(0x4F, 0x4F, 0x4F); // button pressed
pub const GREY_DARK: Color = Color::rgb(0x35, 0x35, 0x35); // button
pub const VIOLET: Color = Color::rgb(0x95, 0x00, 0xCA);
pub const FATAL_ERROR_COLOR: Color = Color::rgb(0xE7, 0x0E, 0x0E);
pub const FATAL_ERROR_HIGHLIGHT_COLOR: Color = Color::rgb(0xFF, 0x41, 0x41);
// Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2;
// UI icons (greyscale).
// Button icons.
include_icon!(ICON_CANCEL, "model_mercury/res/x24.toif");
include_icon!(ICON_CONFIRM, "model_mercury/res/check24.toif");
include_icon!(ICON_UP, "model_mercury/res/caret-up24.toif");
include_icon!(ICON_CORNER_CANCEL, "model_mercury/res/x32.toif");
include_icon!(ICON_CORNER_INFO, "model_mercury/res/info32.toif");
// Checklist symbols.
include_icon!(ICON_LIST_CURRENT, "model_mercury/res/arrow-right16.toif");
include_icon!(ICON_LIST_CHECK, "model_mercury/res/check16.toif");
// Homescreen notifications.
include_icon!(ICON_WARNING40, "model_mercury/res/warning40.toif");
include_icon!(ICON_LOCK_BIG, "model_mercury/res/lock24.toif");
// Text arrows.
include_icon!(ICON_PAGE_NEXT, "model_mercury/res/page-next.toif");
include_icon!(ICON_PAGE_PREV, "model_mercury/res/page-prev.toif");
// Large, three-color icons.
pub const WARN_COLOR: Color = YELLOW;
pub const INFO_COLOR: Color = BLUE;
pub const SUCCESS_COLOR: Color = GREEN;
pub const ERROR_COLOR: Color = RED;
include_icon!(IMAGE_FG_SUCCESS, "model_mercury/res/fg-check48.toif");
include_icon!(IMAGE_BG_CIRCLE, "model_mercury/res/circle48.toif");
// Welcome screen.
include_icon!(ICON_LOGO, "model_mercury/res/lock_full.toif");
include_icon!(ICON_LOGO_EMPTY, "model_mercury/res/lock_empty.toif");
pub const fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: GREY_DARK,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: GREY_MEDIUM,
background_color: BG,
border_color: FG,
border_radius: RADIUS,
border_width: 0,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: GREY_DARK,
background_color: BG,
border_color: BG,
border_radius: RADIUS,
border_width: 0,
},
}
}
pub const fn button_moreinfo() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: BG,
background_color: BG,
border_color: GREY_DARK,
border_radius: RADIUS,
border_width: 2,
},
active: &ButtonStyle {
font: Font::BOLD,
text_color: FG,
button_color: BG,
background_color: BG,
border_color: GREY_MEDIUM,
border_radius: RADIUS,
border_width: 2,
},
disabled: &ButtonStyle {
font: Font::BOLD,
text_color: GREY_LIGHT,
button_color: BG,
background_color: BG,
border_color: GREY_DARK,
border_radius: RADIUS,
border_width: 2,
},
}
}
pub const fn loader_default() -> LoaderStyleSheet {
LoaderStyleSheet {
normal: &LoaderStyle {
icon: None,
loader_color: FG,
background_color: BG,
},
active: &LoaderStyle {
icon: None,
loader_color: GREEN,
background_color: BG,
},
}
}
pub const fn loader_lock_icon() -> LoaderStyleSheet {
LoaderStyleSheet {
normal: &LoaderStyle {
icon: Some((ICON_LOCK_BIG, FG)),
loader_color: FG,
background_color: BG,
},
active: &LoaderStyle {
icon: Some((ICON_LOCK_BIG, FG)),
loader_color: GREEN,
background_color: BG,
},
}
}
pub const TEXT_NORMAL: TextStyle = TextStyle::new(Font::NORMAL, FG, BG, GREY_LIGHT, GREY_LIGHT);
pub const TEXT_DEMIBOLD: TextStyle = TextStyle::new(Font::DEMIBOLD, FG, BG, GREY_LIGHT, GREY_LIGHT);
pub const TEXT_BOLD: TextStyle = TextStyle::new(Font::BOLD, FG, BG, GREY_LIGHT, GREY_LIGHT);
pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, GREY_LIGHT, GREY_LIGHT)
.with_line_breaking(LineBreaking::BreakWordsNoHyphen)
.with_page_breaking(PageBreaking::CutAndInsertEllipsisBoth)
.with_ellipsis_icon(ICON_PAGE_NEXT, 0)
.with_prev_page_icon(ICON_PAGE_PREV, 0);
pub const BUTTON_SPACING: i16 = 6;
pub const CORNER_BUTTON_SIDE: i16 = 44;
pub const RESULT_PADDING: i16 = 6;
pub const RESULT_FOOTER_START: i16 = 171;
/// +----------+
/// | 6 |
/// | +----+ |
/// | 6| | 6|
/// | +----+ |
/// | 6 |
/// +----------+
pub const fn borders() -> Insets {
Insets::new(6, 6, 6, 6)
}

@ -56,6 +56,13 @@ pub trait UIFeaturesBootloader {
fn screen_wipe_fail();
}
#[cfg(all(
feature = "model_mercury",
not(feature = "model_tr"),
not(feature = "model_tt")
))]
pub type ModelUI = crate::ui::model_mercury::ModelMercuryFeatures;
#[cfg(all(feature = "model_tr", not(feature = "model_tt")))]
pub type ModelUI = crate::ui::model_tr::ModelTRFeatures;

Loading…
Cancel
Save