mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-06-05 07:38:46 +00:00
feat(core): copy T2T1 bootloader UI for T3T1
This commit is contained in:
parent
b5fa5a3f3a
commit
cf00726152
2
.github/workflows/core.yml
vendored
2
.github/workflows/core.yml
vendored
@ -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")]
|
||||
|
113
core/embed/rust/src/ui/model_mercury/bootloader/intro.rs
Normal file
113
core/embed/rust/src/ui/model_mercury/bootloader/intro.rs
Normal 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},
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
117
core/embed/rust/src/ui/model_mercury/bootloader/menu.rs
Normal file
117
core/embed/rust/src/ui/model_mercury/bootloader/menu.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
320
core/embed/rust/src/ui/model_mercury/bootloader/mod.rs
Normal file
320
core/embed/rust/src/ui/model_mercury/bootloader/mod.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
60
core/embed/rust/src/ui/model_mercury/bootloader/welcome.rs
Normal file
60
core/embed/rust/src/ui/model_mercury/bootloader/welcome.rs
Normal file
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
254
core/embed/rust/src/ui/model_mercury/component/bl_confirm.rs
Normal file
254
core/embed/rust/src/ui/model_mercury/component/bl_confirm.rs
Normal file
@ -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");
|
||||
}
|
||||
}
|
440
core/embed/rust/src/ui/model_mercury/component/button.rs
Normal file
440
core/embed/rust/src/ui/model_mercury/component/button.rs
Normal file
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
92
core/embed/rust/src/ui/model_mercury/component/error.rs
Normal file
92
core/embed/rust/src/ui/model_mercury/component/error.rs
Normal file
@ -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();
|
||||
}
|
||||
}
|
204
core/embed/rust/src/ui/model_mercury/component/frame.rs
Normal file
204
core/embed/rust/src/ui/model_mercury/component/frame.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
253
core/embed/rust/src/ui/model_mercury/component/loader.rs
Normal file
253
core/embed/rust/src/ui/model_mercury/component/loader.rs
Normal file
@ -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()
|
||||
}
|
||||
}
|
18
core/embed/rust/src/ui/model_mercury/component/mod.rs
Normal file
18
core/embed/rust/src/ui/model_mercury/component/mod.rs
Normal file
@ -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};
|
168
core/embed/rust/src/ui/model_mercury/component/result.rs
Normal file
168
core/embed/rust/src/ui/model_mercury/component/result.rs
Normal file
@ -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());
|
||||
}
|
||||
}
|
22
core/embed/rust/src/ui/model_mercury/constant.rs
Normal file
22
core/embed/rust/src/ui/model_mercury/constant.rs
Normal file
@ -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();
|
33
core/embed/rust/src/ui/model_mercury/mod.rs
Normal file
33
core/embed/rust/src/ui/model_mercury/mod.rs
Normal file
@ -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();
|
||||
}
|
||||
}
|
BIN
core/embed/rust/src/ui/model_mercury/res/arrow-right16.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/arrow-right16.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/caret-up24.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/caret-up24.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/check16.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/check16.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/check24.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/check24.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/check40.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/check40.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/circle48.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/circle48.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/device_name_T.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/device_name_T.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/download32.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/download32.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/fg-check48.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/fg-check48.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/fire24.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/fire24.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/fire32.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/fire32.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/fire40.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/fire40.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/info32.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/info32.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/lock24.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/lock24.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/lock_empty.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/lock_empty.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/lock_full.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/lock_full.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/menu32.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/menu32.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/page-next.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/page-next.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/page-prev.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/page-prev.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/refresh24.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/refresh24.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/start.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/start.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/warning40.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/warning40.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/x24.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/x24.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_mercury/res/x32.toif
Normal file
BIN
core/embed/rust/src/ui/model_mercury/res/x32.toif
Normal file
Binary file not shown.
20
core/embed/rust/src/ui/model_mercury/screens.rs
Normal file
20
core/embed/rust/src/ui/model_mercury/screens.rs
Normal file
@ -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();
|
||||
}
|
281
core/embed/rust/src/ui/model_mercury/theme/bootloader.rs
Normal file
281
core/embed/rust/src/ui/model_mercury/theme/bootloader.rs
Normal file
@ -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);
|
190
core/embed/rust/src/ui/model_mercury/theme/mod.rs
Normal file
190
core/embed/rust/src/ui/model_mercury/theme/mod.rs
Normal file
@ -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…
Reference in New Issue
Block a user