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:
parent
12b3dc23db
commit
5b3db7eca1
@ -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:
|
||||
|
@ -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
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
BIN
core/assets/magic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 224 B |
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -53,3 +53,9 @@ impl TouchEvent {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum USBEvent {
|
||||
/// USB host has connected/disconnected.
|
||||
Connected(bool),
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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()?;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
266
core/embed/rust/src/ui/model_tt/component/homescreen.rs
Normal file
266
core/embed/rust/src/ui/model_tt/component/homescreen.rs
Normal 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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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},
|
||||
|
@ -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)]
|
||||
|
BIN
core/embed/rust/src/ui/model_tt/res/lock.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/lock.toif
Normal file
Binary file not shown.
BIN
core/embed/rust/src/ui/model_tt/res/magic.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/magic.toif
Normal file
Binary file not shown.
@ -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)
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
@ -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)
|
@ -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)
|
@ -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)
|
||||
|
@ -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:
|
||||
|
1
core/src/trezor/ui/layouts/homescreen.py
Normal file
1
core/src/trezor/ui/layouts/homescreen.py
Normal file
@ -0,0 +1 @@
|
||||
from .tt_v2.homescreen import * # noqa: F401,F403
|
126
core/src/trezor/ui/layouts/tt_v2/homescreen.py
Normal file
126
core/src/trezor/ui/layouts/tt_v2/homescreen.py
Normal 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
|
@ -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
|
||||
|
@ -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 ")
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user