1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-24 23:38:09 +00:00

refactor(core/rust/ui): homescreen layouts

[no changelog]
This commit is contained in:
Martin Milata 2022-09-26 23:07:29 +02:00
parent 12b3dc23db
commit 5b3db7eca1
34 changed files with 1782 additions and 1407 deletions

View File

@ -576,12 +576,14 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/common.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/homescreen.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/fido.py'))
if TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/homescreen.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py'))
if EVERYTHING:

View File

@ -530,12 +530,14 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/common.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/homescreen.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/recovery.py'))
if EVERYTHING:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/fido.py'))
if TREZOR_MODEL in ('T',):
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/homescreen.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py'))
if EVERYTHING:

BIN
core/assets/lock-new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

BIN
core/assets/magic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 B

View File

@ -45,7 +45,10 @@ static void _librust_qstrs(void) {
MP_QSTR_request_slip39;
MP_QSTR_select_word;
MP_QSTR_select_word_count;
MP_QSTR_show_busyscreen;
MP_QSTR_show_group_share_success;
MP_QSTR_show_homescreen;
MP_QSTR_show_lockscreen;
MP_QSTR_show_remaining_shares;
MP_QSTR_show_share_words;
MP_QSTR_show_progress;
@ -54,6 +57,7 @@ static void _librust_qstrs(void) {
MP_QSTR_touch_event;
MP_QSTR_button_event;
MP_QSTR_progress_event;
MP_QSTR_usb_event;
MP_QSTR_timer;
MP_QSTR_paint;
MP_QSTR_request_complete_repaint;
@ -100,4 +104,7 @@ static void _librust_qstrs(void) {
MP_QSTR_icon_name;
MP_QSTR_accounts;
MP_QSTR_indeterminate;
MP_QSTR_notification;
MP_QSTR_notification_level;
MP_QSTR_bootscreen;
}

View File

@ -15,6 +15,7 @@ use crate::{
use crate::ui::event::ButtonEvent;
#[cfg(feature = "touch")]
use crate::ui::event::TouchEvent;
use crate::ui::event::USBEvent;
/// Type used by components that do not return any messages.
///
@ -335,6 +336,7 @@ pub enum Event<'a> {
Button(ButtonEvent),
#[cfg(feature = "touch")]
Touch(TouchEvent),
USB(USBEvent),
/// Previously requested timer was triggered. This invalidates the timer
/// token (another timer has to be requested).
Timer(TimerToken),

View File

@ -53,3 +53,9 @@ impl TouchEvent {
Ok(result)
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum USBEvent {
/// USB host has connected/disconnected.
Connected(bool),
}

View File

@ -290,6 +290,10 @@ impl Rect {
self.top_left().center(self.bottom_right())
}
pub const fn top_center(&self) -> Point {
self.top_left().center(self.top_right())
}
pub const fn bottom_center(&self) -> Point {
self.bottom_left().center(self.bottom_right())
}

View File

@ -26,6 +26,7 @@ use crate::{
use crate::ui::event::ButtonEvent;
#[cfg(feature = "touch")]
use crate::ui::event::TouchEvent;
use crate::ui::event::USBEvent;
/// Conversion trait implemented by components that know how to convert their
/// message values into MicroPython `Obj`s.
@ -273,6 +274,7 @@ impl LayoutObj {
Qstr::MP_QSTR_touch_event => obj_fn_var!(4, 4, ui_layout_touch_event).as_obj(),
Qstr::MP_QSTR_button_event => obj_fn_var!(3, 3, ui_layout_button_event).as_obj(),
Qstr::MP_QSTR_progress_event => obj_fn_var!(3, 3, ui_layout_progress_event).as_obj(),
Qstr::MP_QSTR_usb_event => obj_fn_var!(2, 2, ui_layout_usb_event).as_obj(),
Qstr::MP_QSTR_timer => obj_fn_2!(ui_layout_timer).as_obj(),
Qstr::MP_QSTR_paint => obj_fn_1!(ui_layout_paint).as_obj(),
Qstr::MP_QSTR_request_complete_repaint => obj_fn_1!(ui_layout_request_complete_repaint).as_obj(),
@ -412,6 +414,19 @@ extern "C" fn ui_layout_progress_event(n_args: usize, args: *const Obj) -> Obj {
unsafe { util::try_with_args_and_kwargs(n_args, args, &Map::EMPTY, block) }
}
extern "C" fn ui_layout_usb_event(n_args: usize, args: *const Obj) -> Obj {
let block = |args: &[Obj], _kwargs: &Map| {
if args.len() != 2 {
return Err(Error::TypeError);
}
let this: Gc<LayoutObj> = args[0].try_into()?;
let event = USBEvent::Connected(args[1].try_into()?);
let msg = this.obj_event(Event::USB(event))?;
Ok(msg)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, &Map::EMPTY, block) }
}
extern "C" fn ui_layout_timer(this: Obj, token: Obj) -> Obj {
let block = || {
let this: Gc<LayoutObj> = this.try_into()?;

View File

@ -1,8 +1,9 @@
use super::theme;
use crate::ui::{
component::{Child, Component, Event, EventCtx},
component::{text::TextStyle, Child, Component, Event, EventCtx},
display::{self, Color, Font},
geometry::{Insets, Offset, Rect},
util::icon_text_center,
};
pub struct Frame<T, U> {
@ -94,7 +95,6 @@ where
pub struct NotificationFrame<T, U> {
area: Rect,
border: Insets,
icon: &'static [u8],
title: U,
content: Child<T>,
@ -105,18 +105,17 @@ where
T: Component,
U: AsRef<str>,
{
const HEIGHT: i16 = 42;
const HEIGHT: i16 = 32;
const COLOR: Color = theme::YELLOW;
const FONT: Font = Font::BOLD;
const TEXT_OFFSET: Offset = Offset::new(1, -2);
const ICON_SPACE: i16 = 8;
const BORDER: i16 = 8;
pub fn new(icon: &'static [u8], title: U, content: T) -> Self {
Self {
icon,
title,
area: Rect::zero(),
border: theme::borders_notification(),
content: Child::new(content),
}
}
@ -124,6 +123,25 @@ where
pub fn inner(&self) -> &T {
self.content.inner()
}
pub fn paint_notification(area: Rect, icon: &'static [u8], title: &str, color: Color) {
let (area, _) = area
.inset(Insets::uniform(Self::BORDER))
.split_top(Self::HEIGHT);
let style = TextStyle {
background_color: color,
..theme::TEXT_BOLD
};
display::rect_fill_rounded(area, color, theme::BG, 2);
icon_text_center(
area.center(),
icon,
Self::ICON_SPACE,
title,
style,
Self::TEXT_OFFSET,
);
}
}
impl<T, U> Component for NotificationFrame<T, U>
@ -134,10 +152,8 @@ where
type Msg = T::Msg;
fn place(&mut self, bounds: Rect) -> Rect {
let (title_area, content_area) = bounds.split_top(Self::HEIGHT);
let content_area = content_area.inset(self.border);
self.area = title_area;
let content_area = bounds.inset(theme::borders_notification());
self.area = bounds;
self.content.place(content_area);
bounds
}
@ -147,24 +163,7 @@ where
}
fn paint(&mut self) {
let toif_info = unwrap!(display::toif_info(self.icon), "Invalid TOIF data");
let icon_width = toif_info.0.y;
let text_width = Self::FONT.text_width(self.title.as_ref());
let text_height = Self::FONT.text_height();
let text_center =
self.area.center() + Offset::new((icon_width + Self::ICON_SPACE) / 2, text_height / 2);
let icon_center = self.area.center() - Offset::x((text_width + Self::ICON_SPACE) / 2);
display::rect_fill_rounded(self.area, Self::COLOR, theme::BG, 2);
display::text_center(
text_center + Self::TEXT_OFFSET,
self.title.as_ref(),
Self::FONT,
theme::BG,
Self::COLOR,
);
display::icon(icon_center, self.icon, theme::BG, Self::COLOR);
Self::paint_notification(self.area, self.icon, self.title.as_ref(), Self::COLOR);
self.content.paint();
}

View File

@ -0,0 +1,266 @@
use crate::{
time::{Duration, Instant},
ui::{
component::{Component, Empty, Event, EventCtx, Pad, TimerToken},
display::{self, Color, Font},
event::{TouchEvent, USBEvent},
geometry::{Offset, Point, Rect},
model_tt::constant,
util::icon_text_center,
},
};
use super::{theme, Loader, LoaderMsg, NotificationFrame};
const AREA: Rect = constant::screen();
const TOP_CENTER: Point = AREA.top_center();
const LABEL_Y: i16 = 216;
const LOCKED_Y: i16 = 101;
const TAP_Y: i16 = 134;
const HOLD_Y: i16 = 35;
const LOADER_OFFSET: Offset = Offset::y(-10);
const LOADER_DELAY: Duration = Duration::from_millis(500);
const LOADER_DURATION: Duration = Duration::from_millis(2000);
pub struct Homescreen<T> {
label: T,
notification: Option<(T, u8)>,
hold_to_lock: bool,
usb_connected: bool,
loader: Loader,
pad: Pad,
delay: Option<TimerToken>,
}
pub enum HomescreenMsg {
Dismissed,
}
impl<T> Homescreen<T>
where
T: AsRef<str>,
{
pub fn new(label: T, notification: Option<(T, u8)>, hold_to_lock: bool) -> Self {
Self {
label,
notification,
hold_to_lock,
usb_connected: true,
loader: Loader::new().with_durations(LOADER_DURATION, LOADER_DURATION / 3),
pad: Pad::with_background(theme::BG),
delay: None,
}
}
fn level_to_style(level: u8) -> (Color, &'static [u8]) {
match level {
2 => (theme::VIOLET, theme::ICON_MAGIC),
1 => (theme::YELLOW, theme::ICON_WARN),
_ => (theme::RED, theme::ICON_WARN),
}
}
fn paint_notification(&self) {
if !self.usb_connected {
let (color, icon) = Self::level_to_style(0);
NotificationFrame::<Empty, T>::paint_notification(
AREA,
icon,
"NO USB CONNECTION",
color,
);
} else if let Some((notification, level)) = &self.notification {
let (color, icon) = Self::level_to_style(*level);
NotificationFrame::<Empty, T>::paint_notification(
AREA,
icon,
notification.as_ref(),
color,
);
}
}
fn paint_loader(&mut self) {
display::text_center(
TOP_CENTER + Offset::y(HOLD_Y),
"HOLD TO LOCK",
Font::BOLD,
theme::FG,
theme::BG,
);
self.loader.paint()
}
fn event_usb(&mut self, ctx: &mut EventCtx, event: Event) {
if let Event::USB(USBEvent::Connected(is_connected)) = event {
if self.usb_connected != is_connected {
self.usb_connected = is_connected;
ctx.request_paint();
}
}
}
fn event_hold(&mut self, ctx: &mut EventCtx, event: Event) -> bool {
match event {
Event::Touch(TouchEvent::TouchStart(_)) => {
if self.loader.is_animating() {
self.loader.start_growing(ctx, Instant::now());
} else {
self.delay = Some(ctx.request_timer(LOADER_DELAY));
}
}
Event::Touch(TouchEvent::TouchEnd(_)) => {
self.delay = None;
let now = Instant::now();
if self.loader.is_completely_grown(now) {
return true;
}
if self.loader.is_animating() {
self.loader.start_shrinking(ctx, now);
}
}
Event::Timer(token) if Some(token) == self.delay => {
self.delay = None;
self.pad.clear();
self.loader.start_growing(ctx, Instant::now());
}
_ => {}
}
match self.loader.event(ctx, event) {
Some(LoaderMsg::GrownCompletely) => {
// Wait for TouchEnd before returning.
}
Some(LoaderMsg::ShrunkCompletely) => {
self.loader.reset();
self.pad.clear();
ctx.request_paint()
}
None => {}
}
false
}
}
impl<T> Component for Homescreen<T>
where
T: AsRef<str>,
{
type Msg = HomescreenMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.pad.place(AREA);
self.loader.place(AREA.translate(LOADER_OFFSET));
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
Self::event_usb(self, ctx, event);
if self.hold_to_lock {
Self::event_hold(self, ctx, event).then_some(HomescreenMsg::Dismissed)
} else {
None
}
}
fn paint(&mut self) {
self.pad.paint();
if self.loader.is_animating() || self.loader.is_completely_grown(Instant::now()) {
self.paint_loader();
} else {
self.paint_notification();
paint_label(self.label.as_ref(), false);
}
}
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
self.loader.bounds(sink);
sink(self.pad.area);
}
}
#[cfg(feature = "ui_debug")]
impl<T: AsRef<str>> crate::trace::Trace for Homescreen<T> {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("Homescreen");
d.field("label", &self.label.as_ref());
d.close();
}
}
pub struct Lockscreen<T> {
label: T,
bootscreen: bool,
}
impl<T> Lockscreen<T> {
pub fn new(label: T, bootscreen: bool) -> Self {
Lockscreen { label, bootscreen }
}
}
impl<T> Component for Lockscreen<T>
where
T: AsRef<str>,
{
type Msg = HomescreenMsg;
fn place(&mut self, bounds: Rect) -> Rect {
bounds
}
fn event(&mut self, _ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Event::Touch(TouchEvent::TouchEnd(_)) = event {
return Some(HomescreenMsg::Dismissed);
}
None
}
fn paint(&mut self) {
let (locked, tap) = if self.bootscreen {
("NOT CONNECTED", "Tap to connect")
} else {
("LOCKED", "Tap to unlock")
};
icon_text_center(
TOP_CENTER + Offset::y(LOCKED_Y),
theme::ICON_LOCK,
2,
locked,
theme::TEXT_BOLD,
Offset::zero(),
);
display::text_center(
TOP_CENTER + Offset::y(TAP_Y),
tap,
Font::NORMAL,
theme::OFF_WHITE,
theme::BG,
);
paint_label(self.label.as_ref(), true);
}
}
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for Lockscreen<T> {
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
d.open("Lockscreen");
d.close();
}
}
fn paint_label(label: &str, lockscreen: bool) {
let label_color = if lockscreen {
theme::GREY_MEDIUM
} else {
theme::FG
};
display::text_center(
TOP_CENTER + Offset::y(LABEL_Y),
label,
Font::BOLD,
label_color,
theme::BG,
);
}

View File

@ -44,6 +44,16 @@ impl Loader {
}
}
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,

View File

@ -4,6 +4,7 @@ mod fido;
mod fido_icons;
mod frame;
mod hold_to_confirm;
mod homescreen;
mod keyboard;
mod loader;
mod number_input;
@ -20,6 +21,7 @@ pub use dialog::{Dialog, DialogMsg, IconDialog};
pub use fido::{FidoConfirm, FidoMsg};
pub use frame::{Frame, NotificationFrame};
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use homescreen::{Homescreen, HomescreenMsg, Lockscreen};
pub use keyboard::{
bip39::Bip39Input,
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},

View File

@ -43,10 +43,10 @@ use super::{
component::{
Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg,
Dialog, DialogMsg, FidoConfirm, FidoMsg, Frame, HoldToConfirm, HoldToConfirmMsg,
IconDialog, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, NotificationFrame,
NumberInputDialog, NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg,
PinKeyboard, PinKeyboardMsg, Progress, SelectWordCount, SelectWordCountMsg, SelectWordMsg,
Slip39Input, SwipeHoldPage, SwipePage,
Homescreen, HomescreenMsg, IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard,
MnemonicKeyboardMsg, NotificationFrame, NumberInputDialog, NumberInputDialogMsg,
PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, Progress,
SelectWordCount, SelectWordCountMsg, SelectWordMsg, Slip39Input, SwipeHoldPage, SwipePage,
},
theme,
};
@ -295,6 +295,28 @@ where
}
}
impl<T> ComponentMsgObj for Homescreen<T>
where
T: AsRef<str>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
HomescreenMsg::Dismissed => Ok(CANCELLED.as_obj()),
}
}
}
impl<T> ComponentMsgObj for Lockscreen<T>
where
T: AsRef<str>,
{
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
match msg {
HomescreenMsg::Dismissed => Ok(CANCELLED.as_obj()),
}
}
}
extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
@ -1164,6 +1186,52 @@ extern "C" fn new_show_progress(n_args: usize, args: *const Obj, kwargs: *mut Ma
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let label: StrBuffer = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?;
let notification: Option<StrBuffer> =
kwargs.get(Qstr::MP_QSTR_notification)?.try_into_option()?;
let notification_level: u8 = kwargs.get_or(Qstr::MP_QSTR_notification_level, 0)?;
let hold: bool = kwargs.get(Qstr::MP_QSTR_hold)?.try_into()?;
let notification = notification.map(|w| (w, notification_level));
let obj = LayoutObj::new(Homescreen::new(label, notification, hold))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_lockscreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let label: StrBuffer = kwargs.get(Qstr::MP_QSTR_label)?.try_into()?;
let bootscreen: bool = kwargs.get(Qstr::MP_QSTR_bootscreen)?.try_into()?;
let obj = LayoutObj::new(Lockscreen::new(label, bootscreen))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_busyscreen(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let time_ms: u32 = kwargs.get(Qstr::MP_QSTR_time_ms)?.try_into()?;
let obj = LayoutObj::new(Frame::new(
title,
Dialog::new(
Paragraphs::new(Paragraph::new(&theme::TEXT_NORMAL, description).centered()),
Timeout::new(time_ms).map(|msg| {
(matches!(msg, TimeoutMsg::TimedOut)).then(|| CancelConfirmMsg::Cancelled)
}),
),
))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
#[no_mangle]
pub static mp_module_trezorui2: Module = obj_module! {
Qstr::MP_QSTR___name__ => Qstr::MP_QSTR_trezorui2.to_obj(),
@ -1454,7 +1522,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def select_word_count(
/// *,
/// dry_run: bool,
/// ) -> int | trezorui2.CANCELLED:
/// ) -> int | CANCELLED:
/// """Select mnemonic word count from (12, 18, 20, 24, 33)."""
Qstr::MP_QSTR_select_word_count => obj_fn_kw!(0, new_select_word_count).as_obj(),
@ -1482,6 +1550,36 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// description is determined at construction time. If you want multiline descriptions
/// make sure the initial desciption has at least that amount of lines."""
Qstr::MP_QSTR_show_progress => obj_fn_kw!(0, new_show_progress).as_obj(),
/// def show_homescreen(
/// *,
/// label: str,
/// hold: bool,
/// notification: str | None,
/// notification_level: int = 0,
/// skip_first_paint: bool,
/// ) -> CANCELLED:
/// """Idle homescreen."""
Qstr::MP_QSTR_show_homescreen => obj_fn_kw!(0, new_show_homescreen).as_obj(),
/// def show_lockscreen(
/// *,
/// label: str,
/// bootscreen: bool,
/// skip_first_paint: bool,
/// ) -> CANCELLED:
/// """Homescreen for locked device."""
Qstr::MP_QSTR_show_lockscreen => obj_fn_kw!(0, new_show_lockscreen).as_obj(),
/// def show_busyscreen(
/// *,
/// title: str,
/// description: str,
/// time_ms: int,
/// skip_first_paint: bool,
/// ) -> CANCELLED:
/// """Homescreen used for indicating coinjoin in progress."""
Qstr::MP_QSTR_show_busyscreen => obj_fn_kw!(0, new_show_busyscreen).as_obj(),
};
#[cfg(test)]

Binary file not shown.

Binary file not shown.

View File

@ -40,6 +40,7 @@ pub const OFF_WHITE: Color = Color::rgb(0xDE, 0xDE, 0xDE); // very light grey
pub const GREY_LIGHT: Color = Color::rgb(0xA8, 0xA8, 0xA8); // greyish
pub const GREY_MEDIUM: Color = Color::rgb(0x64, 0x64, 0x64);
pub const GREY_DARK: Color = Color::rgb(0x33, 0x33, 0x33); // greyer
pub const VIOLET: Color = Color::rgb(0x9E, 0x27, 0xD6);
// Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2;
@ -58,8 +59,10 @@ pub const ICON_BACK: &[u8] = include_res!("model_tt/res/back.toif");
pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/click.toif");
pub const ICON_NEXT: &[u8] = include_res!("model_tt/res/next.toif");
pub const ICON_WARN: &[u8] = include_res!("model_tt/res/warn-icon.toif");
pub const ICON_MAGIC: &[u8] = include_res!("model_tt/res/magic.toif");
pub const ICON_LIST_CURRENT: &[u8] = include_res!("model_tt/res/current.toif");
pub const ICON_LIST_CHECK: &[u8] = include_res!("model_tt/res/check.toif");
pub const ICON_LOCK: &[u8] = include_res!("model_tt/res/lock.toif");
// Large, three-color icons.
pub const WARN_COLOR: Color = YELLOW;
@ -440,5 +443,5 @@ pub const fn borders_scroll() -> Insets {
}
pub const fn borders_notification() -> Insets {
Insets::new(6, 10, 14, 10)
Insets::new(48, 10, 14, 10)
}

View File

@ -1,3 +1,9 @@
use crate::ui::{
component::text::TextStyle,
display,
geometry::{Offset, Point},
};
pub trait ResultExt {
fn assert_if_debugging_ui(self, message: &str);
}
@ -55,6 +61,32 @@ pub fn animation_disabled() -> bool {
#[cfg(not(feature = "ui_debug"))]
pub fn set_animation_disabled(_disabled: bool) {}
/// Display an icon and a text centered relative to given `Point`.
pub fn icon_text_center(
baseline: Point,
icon: &'static [u8],
space: i16,
text: &str,
style: TextStyle,
text_offset: Offset,
) {
let toif_info = unwrap!(display::toif_info(icon), "Invalid TOIF data");
let icon_width = toif_info.0.y;
let text_width = style.text_font.text_width(text);
let text_height = style.text_font.text_height();
let text_center = baseline + Offset::new((icon_width + space) / 2, text_height / 2);
let icon_center = baseline - Offset::x((text_width + space) / 2);
display::text_center(
text_center + text_offset,
text,
style.text_font,
style.text_color,
style.background_color,
);
display::icon(icon_center, icon, style.text_color, style.background_color);
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -371,7 +371,7 @@ def confirm_recovery(
def select_word_count(
*,
dry_run: bool,
) -> int | trezorui2.CANCELLED:
) -> int | CANCELLED:
"""Select mnemonic word count from (12, 18, 20, 24, 33)."""
@ -401,3 +401,36 @@ def show_progress(
"""Show progress loader. Please note that the number of lines reserved on screen for
description is determined at construction time. If you want multiline descriptions
make sure the initial desciption has at least that amount of lines."""
# rust/src/ui/model_tt/layout.rs
def show_homescreen(
*,
label: str,
hold: bool,
notification: str | None,
notification_level: int = 0,
skip_first_paint: bool,
) -> CANCELLED:
"""Idle homescreen."""
# rust/src/ui/model_tt/layout.rs
def show_lockscreen(
*,
label: str,
bootscreen: bool,
skip_first_paint: bool,
) -> CANCELLED:
"""Homescreen for locked device."""
# rust/src/ui/model_tt/layout.rs
def show_busyscreen(
*,
title: str,
description: str,
time_ms: int,
skip_first_paint: bool,
) -> CANCELLED:
"""Homescreen used for indicating coinjoin in progress."""

View File

@ -153,6 +153,8 @@ trezor.ui.layouts.common
import trezor.ui.layouts.common
trezor.ui.layouts.fido
import trezor.ui.layouts.fido
trezor.ui.layouts.homescreen
import trezor.ui.layouts.homescreen
trezor.ui.layouts.recovery
import trezor.ui.layouts.recovery
trezor.ui.layouts.reset
@ -163,6 +165,8 @@ trezor.ui.layouts.tt_v2
import trezor.ui.layouts.tt_v2
trezor.ui.layouts.tt_v2.fido
import trezor.ui.layouts.tt_v2.fido
trezor.ui.layouts.tt_v2.homescreen
import trezor.ui.layouts.tt_v2.homescreen
trezor.ui.layouts.tt_v2.recovery
import trezor.ui.layouts.tt_v2.recovery
trezor.ui.layouts.tt_v2.reset
@ -293,12 +297,6 @@ apps.debug.load_device
import apps.debug.load_device
apps.homescreen
import apps.homescreen
apps.homescreen.busyscreen
import apps.homescreen.busyscreen
apps.homescreen.homescreen
import apps.homescreen.homescreen
apps.homescreen.lockscreen
import apps.homescreen.lockscreen
apps.management.apply_flags
import apps.management.apply_flags
apps.management.apply_settings

View File

@ -291,12 +291,12 @@ def set_homescreen() -> None:
set_default = workflow.set_default # local_cache_attribute
if storage_cache.is_set(storage_cache.APP_COMMON_BUSY_DEADLINE_MS):
from apps.homescreen.busyscreen import busyscreen
from apps.homescreen import busyscreen
set_default(busyscreen)
elif not config.is_unlocked():
from apps.homescreen.lockscreen import lockscreen
from apps.homescreen import lockscreen
set_default(lockscreen)
@ -306,7 +306,7 @@ def set_homescreen() -> None:
set_default(recovery_homescreen)
else:
from apps.homescreen.homescreen import homescreen
from apps.homescreen import homescreen
set_default(homescreen)

View File

@ -1,47 +1,57 @@
from typing import Any
import storage
import storage.cache
import storage.device
from trezor import ui
from trezor import config, wire
from trezor.ui.layouts.homescreen import Busyscreen, Homescreen, Lockscreen
from apps.base import busy_expiry_ms, lock_device
class HomescreenBase(ui.Layout):
RENDER_INDICATOR: object | None = None
async def busyscreen() -> None:
await Busyscreen(busy_expiry_ms())
def __init__(self) -> None:
super().__init__()
self.label = storage.device.get_label() or "My Trezor"
self.repaint = storage.cache.homescreen_shown is not self.RENDER_INDICATOR
def get_image(self) -> bytes:
from trezor import res
async def homescreen() -> None:
if storage.device.is_initialized():
label = storage.device.get_label()
else:
label = "Go to trezor.io/start"
return storage.device.get_homescreen() or res.load(
"apps/homescreen/res/bg.toif"
)
notification = None
notification_is_error = False
if storage.device.is_initialized() and storage.device.no_backup():
notification = "SEEDLESS"
notification_is_error = True
elif storage.device.is_initialized() and storage.device.unfinished_backup():
notification = "BACKUP FAILED!"
notification_is_error = True
elif storage.device.is_initialized() and storage.device.needs_backup():
notification = "NEEDS BACKUP!"
elif storage.device.is_initialized() and not config.has_pin():
notification = "PIN NOT SET!"
elif storage.device.get_experimental_features():
notification = "EXPERIMENTAL MODE!"
async def __iter__(self) -> Any:
# We need to catch the ui.Cancelled exception that kills us, because that means
# that we will need to draw on screen again after restart.
try:
return await super().__iter__()
except ui.Cancelled:
storage.cache.homescreen_shown = None
raise
await Homescreen(
label=label,
notification=notification,
notification_is_error=notification_is_error,
hold_to_lock=config.has_pin(),
)
lock_device()
def on_render(self) -> None:
if not self.repaint:
return
self.do_render()
self.set_repaint(False)
def do_render(self) -> None:
raise NotImplementedError
async def lockscreen() -> None:
from apps.common.request_pin import can_lock_device
from apps.base import unlock_device
def set_repaint(self, value: bool) -> None:
self.repaint = value
storage.cache.homescreen_shown = None if value else self.RENDER_INDICATOR
def _before_render(self) -> None:
if storage.cache.homescreen_shown is not self.RENDER_INDICATOR:
super()._before_render()
# Only show the lockscreen UI if the device can in fact be locked.
if can_lock_device():
await Lockscreen(label=storage.device.get_label())
# Otherwise proceed directly to unlock() call. If the device is already unlocked,
# it should be a no-op storage-wise, but it resets the internal configuration
# to an unlocked state.
try:
await unlock_device()
except wire.PinCancelled:
pass

View File

@ -1,33 +0,0 @@
from typing import TYPE_CHECKING
import storage.cache as storage_cache
from . import HomescreenBase
if TYPE_CHECKING:
from trezor import loop
async def busyscreen() -> None:
await Busyscreen()
class Busyscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.BUSYSCREEN_ON
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
return self.handle_rendering(), self.handle_input(), self.handle_expiry()
def handle_expiry(self) -> loop.Task: # type: ignore [awaitable-is-generator]
from apps.base import busy_expiry_ms, set_homescreen
from trezor import ui, loop
yield loop.sleep(busy_expiry_ms())
storage_cache.delete(storage_cache.APP_COMMON_BUSY_DEADLINE_MS)
set_homescreen()
raise ui.Result(None)
def do_render(self) -> None:
from trezor.ui.layouts import show_coinjoin
show_coinjoin()

View File

@ -1,142 +0,0 @@
import utime
from micropython import const
from typing import TYPE_CHECKING, Tuple
import storage.cache
from trezor import config, ui
from . import HomescreenBase
if TYPE_CHECKING:
from trezor import loop
_LOADER_DELAY_MS = const(500)
_LOADER_TOTAL_MS = const(2500)
async def homescreen() -> None:
from apps.base import lock_device
await Homescreen()
lock_device()
class Homescreen(HomescreenBase):
RENDER_INDICATOR = storage.cache.HOMESCREEN_ON
def __init__(self) -> None:
from trezor.ui.loader import Loader, LoaderNeutral
import storage.device as storage_device
super().__init__()
if not storage_device.is_initialized():
self.label = "Go to trezor.io/start"
self.loader = Loader(
LoaderNeutral,
_LOADER_TOTAL_MS - _LOADER_DELAY_MS,
-10,
3,
)
self.touch_ms: int | None = None
def create_tasks(self) -> Tuple[loop.AwaitableTask, ...]:
return super().create_tasks() + (self.usb_checker_task(),)
async def usb_checker_task(self) -> None:
from trezor import loop, io
usbcheck = loop.wait(io.USB_CHECK)
while True:
await usbcheck
self.set_repaint(True)
def do_render(self) -> None:
from trezor import utils
from trezor import ui # local_cache_global
import storage.device as storage_device
header_error = ui.header_error # local_cache_attribute
header_warning = ui.header_warning # local_cache_attribute
display = ui.display # local_cache_attribute
model = utils.MODEL # local_cache_attribute
# warning bar on top
if storage_device.is_initialized() and storage_device.no_backup():
header_error("SEEDLESS")
elif storage_device.is_initialized() and storage_device.unfinished_backup():
header_error("BACKUP FAILED!")
elif storage_device.is_initialized() and storage_device.needs_backup():
header_warning("NEEDS BACKUP!")
elif storage_device.is_initialized() and not config.has_pin():
header_warning("PIN NOT SET!")
elif storage_device.get_experimental_features():
header_warning("EXPERIMENTAL MODE!")
else:
display.bar(0, 0, ui.WIDTH, ui.get_header_height(), ui.BG)
# homescreen with shifted avatar and text on bottom
# Differs for each model
if not utils.usb_data_connected():
header_error("NO USB CONNECTION")
# TODO: support homescreen avatar change for R and 1
if model in ("T",):
display.avatar(48, 48 - 10, self.get_image(), ui.WHITE, ui.BLACK)
elif model in ("R",):
icon = "trezor/res/homescreen_model_r.toif" # 92x92 px
display.icon(18, 18, ui.res.load(icon), ui.style.FG, ui.style.BG)
elif model in ("1",):
icon = "trezor/res/homescreen_model_1.toif" # 64x36 px
display.icon(33, 14, ui.res.load(icon), ui.style.FG, ui.style.BG)
label_heights = {"1": 60, "R": 120, "T": 220}
display.text_center(
ui.WIDTH // 2,
label_heights[model],
self.label,
ui.BOLD,
ui.FG,
ui.BG,
)
ui.refresh()
def on_touch_start(self, _x: int, _y: int) -> None:
if self.loader.start_ms is not None:
self.loader.start()
elif config.has_pin():
self.touch_ms = utime.ticks_ms()
def on_touch_end(self, _x: int, _y: int) -> None:
if self.loader.start_ms is not None:
ui.display.clear()
self.set_repaint(True)
self.loader.stop()
self.touch_ms = None
# raise here instead of self.loader.on_finish so as not to send TOUCH_END to the lockscreen
if self.loader.elapsed_ms() >= self.loader.target_ms:
raise ui.Result(None)
def dispatch(self, event: int, x: int, y: int) -> None:
if (
self.touch_ms is not None
and self.touch_ms + _LOADER_DELAY_MS < utime.ticks_ms()
):
self.touch_ms = None
# _loader_start
ui.display.clear()
ui.display.text_center(
ui.WIDTH // 2, 35, "Hold to lock", ui.BOLD, ui.FG, ui.BG
)
self.loader.start()
# END _loader_start
if event is ui.RENDER and self.loader.start_ms is not None:
self.loader.dispatch(event, x, y)
else:
super().dispatch(event, x, y)

View File

@ -1,67 +0,0 @@
import storage.cache
from trezor import loop, ui
from . import HomescreenBase
async def lockscreen() -> None:
from trezor import wire
from apps.common.request_pin import can_lock_device
from apps.base import unlock_device
# Only show the lockscreen UI if the device can in fact be locked.
if can_lock_device():
await Lockscreen()
# Otherwise proceed directly to unlock() call. If the device is already unlocked,
# it should be a no-op storage-wise, but it resets the internal configuration
# to an unlocked state.
try:
await unlock_device()
except wire.PinCancelled:
pass
class Lockscreen(HomescreenBase):
BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW
RENDER_SLEEP = loop.SLEEP_FOREVER
RENDER_INDICATOR = storage.cache.LOCKSCREEN_ON
def __init__(self, bootscreen: bool = False) -> None:
if bootscreen:
self.BACKLIGHT_LEVEL = ui.BACKLIGHT_NORMAL
self.lock_label = "Not connected"
self.tap_label = "Tap to connect"
else:
self.lock_label = "Locked"
self.tap_label = "Tap to unlock"
super().__init__()
def do_render(self) -> None:
from trezor import res
from trezor import ui # local_cache_global
display = ui.display # local_cache_attribute
title_grey = ui.TITLE_GREY # local_cache_attribute
bg = ui.BG # local_cache_attribute
# homescreen with label text on top
display.text_center(ui.WIDTH // 2, 35, self.label, ui.BOLD, title_grey, bg)
display.avatar(48, 48, self.get_image(), ui.WHITE, ui.BLACK)
# lock bar
display.bar_radius(40, 100, 160, 40, title_grey, bg, 4)
display.bar_radius(42, 102, 156, 36, bg, title_grey, 4)
display.text_center(
ui.WIDTH // 2, 128, self.lock_label, ui.BOLD, title_grey, bg
)
# "tap to unlock"
display.text_center(
ui.WIDTH // 2 + 10, 220, self.tap_label, ui.BOLD, title_grey, bg
)
display.icon(45, 202, res.load(ui.ICON_CLICK), title_grey, bg)
def on_touch_end(self, _x: int, _y: int) -> None:
raise ui.Result(None)

View File

@ -15,7 +15,7 @@ if TYPE_CHECKING:
async def recovery_homescreen() -> None:
from trezor import workflow
from apps.homescreen.homescreen import homescreen
from apps.homescreen import homescreen
if not storage_recovery.is_in_progress():
workflow.set_default(homescreen)

View File

@ -2,13 +2,13 @@ import storage
import storage.device
from trezor import config, log, loop, ui, utils, wire
from trezor.pin import show_pin_timeout
from trezor.ui.layouts.homescreen import Lockscreen
from apps.common.request_pin import can_lock_device, verify_user_pin
from apps.homescreen.lockscreen import Lockscreen
async def bootscreen() -> None:
lockscreen = Lockscreen(bootscreen=True)
lockscreen = Lockscreen(label=storage.device.get_label(), bootscreen=True)
ui.display.orientation(storage.device.get_rotation())
while True:
try:

View File

@ -0,0 +1 @@
from .tt_v2.homescreen import * # noqa: F401,F403

View File

@ -0,0 +1,126 @@
from typing import TYPE_CHECKING
import storage.cache as storage_cache
from trezor import ui
import trezorui2
from . import _RustLayout
if TYPE_CHECKING:
from trezor import io
from typing import Any, Tuple
class HomescreenBase(_RustLayout):
RENDER_INDICATOR: object | None = None
def __init__(self, layout: Any) -> None:
super().__init__(layout=layout)
self.is_connected = True
async def __iter__(self) -> Any:
# We need to catch the ui.Cancelled exception that kills us, because that means
# that we will need to draw on screen again after restart.
try:
return await super().__iter__()
except ui.Cancelled:
storage_cache.homescreen_shown = None
raise
# In __debug__ mode, ignore {confirm,swipe,input}_signal.
def create_tasks(self) -> tuple[loop.AwaitableTask, ...]:
return self.handle_timers(), self.handle_input_and_rendering()
class Homescreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.HOMESCREEN_ON
def __init__(
self,
label: str | None,
notification: str | None,
notification_is_error: bool,
hold_to_lock: bool,
) -> None:
level = 1
if notification is not None:
notification = notification.rstrip("!")
if "EXPERIMENTAL" in notification:
level = 2
elif notification_is_error:
level = 0
super().__init__(
layout=trezorui2.show_homescreen(
label=label or "My Trezor",
notification=notification,
notification_level=level,
hold=hold_to_lock,
),
)
async def usb_checker_task(self) -> None:
from trezor import io, loop
usbcheck = loop.wait(io.USB_CHECK)
while True:
is_connected = await usbcheck
if is_connected != self.is_connected:
self.is_connected = is_connected
self.layout.usb_event(is_connected)
self.layout.paint()
storage_cache.homescreen_shown = None
def create_tasks(self) -> Tuple[loop.AwaitableTask, ...]:
return super().create_tasks() + (self.usb_checker_task(),)
class Lockscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.LOCKSCREEN_ON
BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW
def __init__(
self,
label: str | None,
bootscreen: bool = False,
) -> None:
self.bootscreen = bootscreen
if bootscreen:
self.BACKLIGHT_LEVEL = ui.BACKLIGHT_NORMAL
super().__init__(
layout=trezorui2.show_lockscreen(
label=label or "My Trezor",
bootscreen=bootscreen,
),
)
async def __iter__(self) -> Any:
result = await super().__iter__()
if self.bootscreen:
self.request_complete_repaint()
return result
class Busyscreen(HomescreenBase):
RENDER_INDICATOR = storage_cache.BUSYSCREEN_ON
def __init__(self, delay_ms: int) -> None:
super().__init__(
layout=trezorui2.show_busyscreen(
title="PLEASE WAIT",
description="CoinJoin in progress.\n\nDo not disconnect your\nTrezor.",
time_ms=delay_ms,
)
)
async def __iter__(self) -> Any:
from apps.base import set_homescreen
# Handle timeout.
result = await super().__iter__()
assert result == trezorui2.CANCELLED
storage_cache.delete(storage_cache.APP_COMMON_BUSY_DEADLINE_MS)
set_homescreen()
return result

View File

@ -17,6 +17,7 @@ def toif_convert(infile, outfile):
toif_convert.py somefile.jpg outfile.toif
toif_convert.py infile.toif outfile.png
\b
# ensure gray-scale output TOIF
mogrify -colorspace gray icon.png
toif_convert.py icon.png icon.toif

View File

@ -85,4 +85,4 @@ def enter_shares(debug: "DebugLink", shares: list[str]) -> None:
def finalize(debug: "DebugLink") -> None:
layout = debug.click(buttons.OK, wait=True)
assert layout.text == "Homescreen"
assert layout.text.startswith("< Homescreen ")

View File

@ -57,7 +57,7 @@ def set_autolock_delay(device_handler: "BackgroundDeviceHandler", delay_ms: int)
debug.click(buttons.OK)
layout = debug.wait_layout()
assert layout.text == "Homescreen"
assert layout.text.startswith("< Homescreen")
assert device_handler.result() == "Settings applied"
@ -136,7 +136,7 @@ def test_dryrun_locks_at_number_of_words(device_handler: "BackgroundDeviceHandle
# wait for autolock to trigger
time.sleep(10.1)
layout = debug.wait_layout()
assert layout.text == "Lockscreen"
assert layout.text.startswith("< Lockscreen")
with pytest.raises(exceptions.Cancelled):
device_handler.result()
@ -171,7 +171,7 @@ def test_dryrun_locks_at_word_entry(device_handler: "BackgroundDeviceHandler"):
assert layout.text == "< MnemonicKeyboard >"
time.sleep(10.1)
layout = debug.wait_layout()
assert layout.text == "Lockscreen"
assert layout.text.startswith("< Lockscreen")
with pytest.raises(exceptions.Cancelled):
device_handler.result()

View File

@ -69,7 +69,7 @@ def test_abort(emulator: Emulator):
assert layout.get_title() == "ABORT RECOVERY"
layout = debug.click(buttons.OK, wait=True)
assert layout.text == "Homescreen"
assert layout.text.startswith("< Homescreen")
features = device_handler.features()
assert features.recovery_mode is False
@ -212,7 +212,7 @@ def test_recovery_multiple_resets(emulator: Emulator):
enter_shares_with_restarts(debug)
debug = device_handler.debuglink()
layout = debug.read_layout()
assert layout.text == "Homescreen"
assert layout.text.startswith("< Homescreen")
features = device_handler.features()
assert features.initialized is True

File diff suppressed because it is too large Load Diff