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:
parent
d3469f8310
commit
e76a394c4b
@ -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:
|
||||
|
@ -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
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 |
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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()?;
|
||||
|
@ -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, 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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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},
|
||||
|
@ -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)]
|
||||
|
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.
@ -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)
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
)
|
@ -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)
|
@ -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)
|
@ -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
|
||||
|
@ -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:
|
||||
|
6
core/src/trezor/ui/layouts/homescreen.py
Normal file
6
core/src/trezor/ui/layouts/homescreen.py
Normal 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
|
217
core/src/trezor/ui/layouts/tt/homescreen.py
Normal file
217
core/src/trezor/ui/layouts/tt/homescreen.py
Normal 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."
|
||||
)
|
116
core/src/trezor/ui/layouts/tt_v2/homescreen.py
Normal file
116
core/src/trezor/ui/layouts/tt_v2/homescreen.py
Normal 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
|
@ -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
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user