1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-23 06:58:13 +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 d3469f8310
commit e76a394c4b
33 changed files with 1981 additions and 1342 deletions

View File

@ -587,6 +587,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/__init__.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:
@ -595,6 +596,7 @@ if FROZEN:
if TREZOR_MODEL in ('T',) and UI2:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
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:
@ -604,6 +606,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/homescreen.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/recovery.py'))
if EVERYTHING:

View File

@ -541,6 +541,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/__init__.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:
@ -549,6 +550,7 @@ if FROZEN:
if TREZOR_MODEL in ('T',) and UI2:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
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:
@ -558,6 +560,7 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/__init__.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/homescreen.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/reset.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/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

@ -42,13 +42,17 @@ 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_attach_timer_fn;
MP_QSTR_touch_event;
MP_QSTR_button_event;
MP_QSTR_usb_event;
MP_QSTR_timer;
MP_QSTR_paint;
MP_QSTR_request_complete_repaint;
@ -90,4 +94,7 @@ static void _librust_qstrs(void) {
MP_QSTR_active;
MP_QSTR_info_button;
MP_QSTR_time_ms;
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 {
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,15 @@ impl TouchEvent {
Ok(result)
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum USBEvent {
/// USB host has connected/disconnected.
Connected(bool),
}
impl USBEvent {
pub fn new(connected: bool) -> Self {
Self::Connected(connected)
}
}

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

@ -25,6 +25,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.
@ -280,6 +281,7 @@ impl LayoutObj {
Qstr::MP_QSTR_attach_timer_fn => obj_fn_2!(ui_layout_attach_timer_fn).as_obj(),
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_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(),
@ -403,6 +405,19 @@ extern "C" fn ui_layout_button_event(_n_args: usize, _args: *const Obj) -> Obj {
Obj::const_none()
}
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::new(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, u32)>,
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, u32)>, 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: u32) -> (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

@ -43,6 +43,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

@ -2,6 +2,7 @@ mod button;
mod dialog;
mod frame;
mod hold_to_confirm;
mod homescreen;
mod keyboard;
mod loader;
mod number_input;
@ -16,6 +17,7 @@ pub use button::{
pub use dialog::{Dialog, DialogMsg, IconDialog};
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

@ -31,11 +31,11 @@ use crate::{
use super::{
component::{
Bip39Input, Button, ButtonMsg, ButtonStyleSheet, CancelConfirmMsg, CancelInfoConfirmMsg,
Dialog, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, IconDialog, MnemonicInput,
MnemonicKeyboard, MnemonicKeyboardMsg, NotificationFrame, NumberInputDialog,
NumberInputDialogMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboard,
PinKeyboardMsg, SelectWordCount, SelectWordCountMsg, SelectWordMsg, Slip39Input,
SwipeHoldPage, SwipePage,
Dialog, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, Homescreen, HomescreenMsg,
IconDialog, Lockscreen, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg,
NotificationFrame, NumberInputDialog, NumberInputDialogMsg, PassphraseKeyboard,
PassphraseKeyboardMsg, PinKeyboard, PinKeyboardMsg, SelectWordCount, SelectWordCountMsg,
SelectWordMsg, Slip39Input, SwipeHoldPage, SwipePage,
},
theme,
};
@ -259,6 +259,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()?;
@ -982,6 +1004,54 @@ extern "C" fn new_show_remaining_shares(n_args: usize, args: *const Obj, kwargs:
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: u32 = 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()
.add(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(),
@ -1260,6 +1330,33 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// ) -> int:
/// """Shows SLIP39 state after info button is pressed on `confirm_recovery`."""
Qstr::MP_QSTR_show_remaining_shares => obj_fn_kw!(0, new_show_remaining_shares).as_obj(),
/// def show_homescreen(
/// *,
/// label: str,
/// hold: bool,
/// notification: str | None,
/// notification_level: int = 0,
/// ) -> trezorui2.CANCELLED:
/// """Idle homescreen."""
Qstr::MP_QSTR_show_homescreen => obj_fn_kw!(0, new_show_homescreen).as_obj(),
/// def show_lockscreen(
/// *,
/// label: str,
/// bootscreen: bool,
/// ) -> trezorui2.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,
/// ) -> trezorui2.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

@ -34,6 +34,7 @@ pub const OFF_WHITE: Color = Color::rgb(222, 222, 222); // very light grey
pub const GREY_LIGHT: Color = Color::rgb(168, 168, 168); // greyish
pub const GREY_MEDIUM: Color = Color::rgb(100, 100, 100);
pub const GREY_DARK: Color = Color::rgb(51, 51, 51); // greyer
pub const VIOLET: Color = Color::rgb(158, 39, 214);
// Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2;
@ -52,8 +53,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, color icons.
pub const IMAGE_WARN: &[u8] = include_res!("model_tt/res/warn.toif");
@ -454,5 +457,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);
}
@ -31,6 +37,32 @@ pub fn u32_to_str(num: u32, buffer: &mut [u8]) -> Option<&str> {
}
}
/// 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

@ -348,3 +348,33 @@ def show_remaining_shares(
pages: Iterable[tuple[str, str]],
) -> int:
"""Shows SLIP39 state after info button is pressed on `confirm_recovery`."""
# rust/src/ui/model_tt/layout.rs
def show_homescreen(
*,
label: str,
hold: bool,
notification: str | None,
notification_level: int = 0,
) -> trezorui2.CANCELLED:
"""Idle homescreen."""
# rust/src/ui/model_tt/layout.rs
def show_lockscreen(
*,
label: str,
bootscreen: bool,
) -> trezorui2.CANCELLED:
"""Homescreen for locked device."""
# rust/src/ui/model_tt/layout.rs
def show_busyscreen(
*,
title: str,
description: str,
time_ms: int,
) -> trezorui2.CANCELLED:
"""Homescreen used for indicating coinjoin in progress."""

View File

@ -199,6 +199,8 @@ trezor.ui.layouts.altcoin
import trezor.ui.layouts.altcoin
trezor.ui.layouts.common
import trezor.ui.layouts.common
trezor.ui.layouts.homescreen
import trezor.ui.layouts.homescreen
trezor.ui.layouts.recovery
import trezor.ui.layouts.recovery
trezor.ui.layouts.reset
@ -211,6 +213,8 @@ trezor.ui.layouts.tt
import trezor.ui.layouts.tt
trezor.ui.layouts.tt.altcoin
import trezor.ui.layouts.tt.altcoin
trezor.ui.layouts.tt.homescreen
import trezor.ui.layouts.tt.homescreen
trezor.ui.layouts.tt.recovery
import trezor.ui.layouts.tt.recovery
trezor.ui.layouts.tt.reset
@ -219,6 +223,8 @@ trezor.ui.layouts.tt_v2
import trezor.ui.layouts.tt_v2
trezor.ui.layouts.tt_v2.altcoin
import trezor.ui.layouts.tt_v2.altcoin
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
@ -351,12 +357,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:
import storage # workaround for https://github.com/microsoft/pyright/issues/2685
if storage.cache.is_set(storage.cache.APP_COMMON_BUSY_DEADLINE_MS):
from apps.homescreen.busyscreen import busyscreen
from apps.homescreen import busyscreen
workflow.set_default(busyscreen)
elif not config.is_unlocked():
from apps.homescreen.lockscreen import lockscreen
from apps.homescreen import lockscreen
workflow.set_default(lockscreen)
@ -306,7 +306,7 @@ def set_homescreen() -> None:
workflow.set_default(recovery_homescreen)
else:
from apps.homescreen.homescreen import homescreen
from apps.homescreen import homescreen
workflow.set_default(homescreen)

View File

@ -1,45 +1,57 @@
from typing import Any
import storage
import storage.cache
import storage.device
from trezor import res, 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:
return storage.device.get_homescreen() or res.load(
"apps/homescreen/res/bg.toif"
)
async def homescreen() -> None:
if storage.device.is_initialized():
label = storage.device.get_label()
else:
label = "Go to trezor.io/start"
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
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!"
def on_render(self) -> None:
if not self.repaint:
return
self.do_render()
self.set_repaint(False)
await Homescreen(
label=label,
notification=notification,
notification_is_error=notification_is_error,
hold_to_lock=config.has_pin(),
)
lock_device()
def do_render(self) -> None:
raise NotImplementedError
def set_repaint(self, value: bool) -> None:
self.repaint = value
storage.cache.homescreen_shown = None if value else self.RENDER_INDICATOR
async def lockscreen() -> None:
from apps.common.request_pin import can_lock_device
from apps.base import unlock_device
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,29 +0,0 @@
import storage.cache
from trezor import loop, ui
from trezor.ui.layouts import draw_simple_text
from apps.base import busy_expiry_ms, set_homescreen
from . import HomescreenBase
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]
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:
draw_simple_text(
"Please wait", "CoinJoin in progress.\n\nDo not disconnect your\nTrezor."
)

View File

@ -1,120 +0,0 @@
import utime
from micropython import const
from typing import Tuple
import storage
import storage.cache
import storage.device
from trezor import config, io, loop, ui, utils
from trezor.ui.loader import Loader, LoaderNeutral
from apps.base import lock_device
from . import HomescreenBase
_LOADER_DELAY_MS = const(500)
_LOADER_TOTAL_MS = const(2500)
async def homescreen() -> None:
await Homescreen()
lock_device()
class Homescreen(HomescreenBase):
RENDER_INDICATOR = storage.cache.HOMESCREEN_ON
def __init__(self) -> None:
super().__init__()
if not storage.device.is_initialized():
self.label = "Go to trezor.io/start"
self.loader = Loader(
style=LoaderNeutral,
target_ms=_LOADER_TOTAL_MS - _LOADER_DELAY_MS,
offset_y=-10,
reverse_speedup=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:
usbcheck = loop.wait(io.USB_CHECK)
while True:
await usbcheck
self.set_repaint(True)
def do_render(self) -> None:
# warning bar on top
if storage.device.is_initialized() and storage.device.no_backup():
ui.header_error("SEEDLESS")
elif storage.device.is_initialized() and storage.device.unfinished_backup():
ui.header_error("BACKUP FAILED!")
elif storage.device.is_initialized() and storage.device.needs_backup():
ui.header_warning("NEEDS BACKUP!")
elif storage.device.is_initialized() and not config.has_pin():
ui.header_warning("PIN NOT SET!")
elif storage.device.get_experimental_features():
ui.header_warning("EXPERIMENTAL MODE!")
else:
ui.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():
ui.header_error("NO USB CONNECTION")
# TODO: support homescreen avatar change for R and 1
if utils.MODEL in ("T",):
ui.display.avatar(48, 48 - 10, self.get_image(), ui.WHITE, ui.BLACK)
elif utils.MODEL in ("R",):
icon = "trezor/res/homescreen_model_r.toif" # 92x92 px
ui.display.icon(18, 18, ui.res.load(icon), ui.style.FG, ui.style.BG)
elif utils.MODEL in ("1",):
icon = "trezor/res/homescreen_model_1.toif" # 64x36 px
ui.display.icon(33, 14, ui.res.load(icon), ui.style.FG, ui.style.BG)
label_heights = {"1": 60, "R": 120, "T": 220}
ui.display.text_center(
ui.WIDTH // 2, label_heights[utils.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 _loader_start(self) -> None:
ui.display.clear()
ui.display.text_center(ui.WIDTH // 2, 35, "Hold to lock", ui.BOLD, ui.FG, ui.BG)
self.loader.start()
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
self._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,60 +0,0 @@
import storage.cache
from trezor import loop, res, ui, wire
from . import HomescreenBase
async def lockscreen() -> None:
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:
# homescreen with label text on top
ui.display.text_center(
ui.WIDTH // 2, 35, self.label, ui.BOLD, ui.TITLE_GREY, ui.BG
)
ui.display.avatar(48, 48, self.get_image(), ui.WHITE, ui.BLACK)
# lock bar
ui.display.bar_radius(40, 100, 160, 40, ui.TITLE_GREY, ui.BG, 4)
ui.display.bar_radius(42, 102, 156, 36, ui.BG, ui.TITLE_GREY, 4)
ui.display.text_center(
ui.WIDTH // 2, 128, self.lock_label, ui.BOLD, ui.TITLE_GREY, ui.BG
)
# "tap to unlock"
ui.display.text_center(
ui.WIDTH // 2 + 10, 220, self.tap_label, ui.BOLD, ui.TITLE_GREY, ui.BG
)
ui.display.icon(45, 202, res.load(ui.ICON_CLICK), ui.TITLE_GREY, ui.BG)
def on_touch_end(self, _x: int, _y: int) -> None:
raise ui.Result(None)

View File

@ -11,7 +11,7 @@ from trezor.messages import Success
from trezor.ui.layouts import show_success
from apps.common import mnemonic
from apps.homescreen.homescreen import homescreen
from apps.homescreen import homescreen
from .. import backup_types
from . import layout, recover

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,6 @@
from . import UI2
if UI2:
from .tt_v2.homescreen import * # noqa: F401,F403
else:
from .tt.homescreen import * # noqa: F401,F403

View File

@ -0,0 +1,217 @@
import utime
from micropython import const
from typing import Any, Tuple
import storage.cache
import storage.device
from trezor import io, loop, res, ui, utils
from trezor.ui.loader import Loader, LoaderNeutral
from apps.base import set_homescreen
from . import draw_simple_text
class HomescreenBase(ui.Layout):
RENDER_INDICATOR: object | None = None
def __init__(self, label: str | None) -> None:
super().__init__()
self.label = label or "My Trezor"
self.repaint = storage.cache.homescreen_shown is not self.RENDER_INDICATOR
def get_image(self) -> bytes:
return storage.device.get_homescreen() or res.load(
"apps/homescreen/res/bg.toif"
)
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
def on_render(self) -> None:
if not self.repaint:
return
self.do_render()
self.set_repaint(False)
def do_render(self) -> None:
raise NotImplementedError
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()
_LOADER_DELAY_MS = const(500)
_LOADER_TOTAL_MS = const(2500)
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:
super().__init__(label=label)
self.notification = notification
self.notification_is_error = notification_is_error
self.hold_to_lock = hold_to_lock
self.loader = Loader(
style=LoaderNeutral,
target_ms=_LOADER_TOTAL_MS - _LOADER_DELAY_MS,
offset_y=-10,
reverse_speedup=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:
usbcheck = loop.wait(io.USB_CHECK)
while True:
await usbcheck
self.set_repaint(True)
def do_render(self) -> None:
# warning bar on top
if not utils.usb_data_connected():
ui.header_error("NO USB CONNECTION")
elif self.notification is not None:
if self.notification_is_error:
ui.header_error(self.notification)
else:
ui.header_warning(self.notification)
else:
ui.display.bar(0, 0, ui.WIDTH, ui.get_header_height(), ui.BG)
# homescreen with shifted avatar and text on bottom
# Differs for each model
# TODO: support homescreen avatar change for R and 1
if utils.MODEL in ("T",):
ui.display.avatar(48, 48 - 10, self.get_image(), ui.WHITE, ui.BLACK)
elif utils.MODEL in ("R",):
icon = "trezor/res/homescreen_model_r.toif" # 92x92 px
ui.display.icon(18, 18, ui.res.load(icon), ui.style.FG, ui.style.BG)
elif utils.MODEL in ("1",):
icon = "trezor/res/homescreen_model_1.toif" # 64x36 px
ui.display.icon(33, 14, ui.res.load(icon), ui.style.FG, ui.style.BG)
label_heights = {"1": 60, "R": 120, "T": 220}
ui.display.text_center(
ui.WIDTH // 2, label_heights[utils.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 self.hold_to_lock:
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 _loader_start(self) -> None:
ui.display.clear()
ui.display.text_center(ui.WIDTH // 2, 35, "Hold to lock", ui.BOLD, ui.FG, ui.BG)
self.loader.start()
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
self._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)
class Lockscreen(HomescreenBase):
BACKLIGHT_LEVEL = ui.BACKLIGHT_LOW
RENDER_SLEEP = loop.SLEEP_FOREVER
RENDER_INDICATOR = storage.cache.LOCKSCREEN_ON
def __init__(self, label: str | None, 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__(label=label)
def do_render(self) -> None:
# homescreen with label text on top
ui.display.text_center(
ui.WIDTH // 2, 35, self.label, ui.BOLD, ui.TITLE_GREY, ui.BG
)
ui.display.avatar(48, 48, self.get_image(), ui.WHITE, ui.BLACK)
# lock bar
ui.display.bar_radius(40, 100, 160, 40, ui.TITLE_GREY, ui.BG, 4)
ui.display.bar_radius(42, 102, 156, 36, ui.BG, ui.TITLE_GREY, 4)
ui.display.text_center(
ui.WIDTH // 2, 128, self.lock_label, ui.BOLD, ui.TITLE_GREY, ui.BG
)
# "tap to unlock"
ui.display.text_center(
ui.WIDTH // 2 + 10, 220, self.tap_label, ui.BOLD, ui.TITLE_GREY, ui.BG
)
ui.display.icon(45, 202, res.load(ui.ICON_CLICK), ui.TITLE_GREY, ui.BG)
def on_touch_end(self, _x: int, _y: int) -> None:
raise ui.Result(None)
class Busyscreen(HomescreenBase):
RENDER_INDICATOR = storage.cache.BUSYSCREEN_ON
def __init__(self, delay_ms: int):
super().__init__(label=None)
self.delay_ms = delay_ms
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]
yield loop.sleep(self.delay_ms)
storage.cache.delete(storage.cache.APP_COMMON_BUSY_DEADLINE_MS)
set_homescreen()
raise ui.Result(None)
def do_render(self) -> None:
draw_simple_text(
"Please wait", "CoinJoin in progress.\n\nDo not disconnect your\nTrezor."
)

View File

@ -0,0 +1,116 @@
from typing import Any, Tuple
import storage.cache
import storage.device
from trezor import io, loop, ui
import trezorui2
from apps.base import set_homescreen
from . import _RustLayout
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
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:
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:
# 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

@ -400,6 +400,11 @@ HEXDATA = "0123456789abcd000023456789abcd010003456789abcd020000456789abcd0300000
)
@pytest.mark.skip_t1
def test_signtx_data_pagination(client: Client, flow):
# prevent this test from getting stuck on UI2
import os
assert os.getenv("UI2") != "1"
with client:
client.watch_layout()
client.set_input_flow(flow(client))

File diff suppressed because it is too large Load Diff