parent
b5fa5a3f3a
commit
cf00726152
@ -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();
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
}
|
Loading…
Reference in new issue