feat(core): add loader to homescreen when locking the device for TS3

pull/3518/head
grdddj 4 months ago committed by Jiří Musil
parent 5d8e56ac2a
commit 76c547bb91

@ -0,0 +1 @@
[T2B1] Add loader to homescreen when locking the device

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

@ -47,6 +47,10 @@ pub trait Component {
/// Component can also optionally return a message as a result of the
/// interaction.
///
/// For all components to work properly (e.g. react to `ctx.request_paint`),
/// it is required to call `event` function to them, even if they never
/// return a message.
///
/// No painting should be done in this phase.
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg>;

@ -5,6 +5,7 @@ use crate::{
component::{Component, Event, EventCtx, Never},
constant,
display::{self, Color, Font, Icon},
event::PhysicalButton,
geometry::{Alignment2D, Offset, Point, Rect},
},
};
@ -20,6 +21,15 @@ pub enum ButtonPos {
Right,
}
impl From<PhysicalButton> for ButtonPos {
fn from(btn: PhysicalButton) -> Self {
match btn {
PhysicalButton::Left => ButtonPos::Left,
PhysicalButton::Right => ButtonPos::Right,
}
}
}
pub struct Button<T>
where
T: StringType,

@ -46,6 +46,9 @@ pub enum ButtonControllerMsg {
Triggered(ButtonPos, bool),
/// Button was pressed and held for longer time (not released yet).
LongPressed(ButtonPos),
/// Hold-to-confirm button was released prematurely - without triggering
/// LongPressed.
ReleasedWithoutLongPress(ButtonPos),
}
/// Defines what kind of button should be currently used.
@ -186,10 +189,11 @@ where
self.long_pressed_timer = None;
Some(ButtonControllerMsg::Triggered(self.pos, long_press))
}
_ => {
ButtonType::HoldToConfirm(_) => {
self.hold_ended(ctx);
None
Some(ButtonControllerMsg::ReleasedWithoutLongPress(self.pos))
}
_ => None,
}
}
@ -269,6 +273,8 @@ where
button_area: Rect,
/// Handling optional ignoring of buttons after pressing the other button.
ignore_btn_delay: Option<IgnoreButtonDelay>,
/// Whether to count with middle button
handle_middle_button: bool,
}
impl<T> ButtonController<T>
@ -276,6 +282,7 @@ where
T: StringType,
{
pub fn new(btn_layout: ButtonLayout<T>) -> Self {
let handle_middle_button = btn_layout.btn_middle.is_some();
Self {
pad: Pad::with_background(theme::BG).with_clear(),
left_btn: ButtonContainer::new(ButtonPos::Left, btn_layout.btn_left),
@ -284,6 +291,7 @@ where
state: ButtonState::Nothing,
button_area: Rect::zero(),
ignore_btn_delay: None,
handle_middle_button,
}
}
@ -296,6 +304,7 @@ where
/// Updating all the three buttons to the wanted states.
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
self.handle_middle_button = btn_layout.btn_middle.is_some();
self.pad.clear();
self.left_btn.set(btn_layout.btn_left, self.button_area);
self.middle_btn.set(btn_layout.btn_middle, self.button_area);
@ -471,13 +480,21 @@ where
return None;
}
}
self.got_pressed(ctx, ButtonPos::Middle);
self.middle_hold_started(ctx);
(
// ↓ ↓
ButtonState::BothDown,
Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)),
)
// ↓ ↓
if self.handle_middle_button {
self.got_pressed(ctx, ButtonPos::Middle);
self.middle_hold_started(ctx);
(
ButtonState::BothDown,
Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)),
)
} else {
self.got_pressed(ctx, b.into());
(
ButtonState::BothDown,
Some(ButtonControllerMsg::Pressed(b.into())),
)
}
}
_ => (self.state, None),
},
@ -487,7 +504,14 @@ where
ButtonEvent::ButtonReleased(b) => {
self.middle_btn.hold_ended(ctx);
// _ ↓ | ↓ _
(ButtonState::OneReleased(b), None)
if self.handle_middle_button {
(ButtonState::OneReleased(b), None)
} else {
(
ButtonState::OneReleased(b),
Some(ButtonControllerMsg::Triggered(b.into(), false)),
)
}
}
_ => (self.state, None),
},
@ -507,7 +531,14 @@ where
ignore_btn_delay.make_button_clickable(ButtonPos::Left);
ignore_btn_delay.make_button_clickable(ButtonPos::Right);
}
(ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx))
if self.handle_middle_button {
(ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx))
} else {
(
ButtonState::Nothing,
Some(ButtonControllerMsg::Triggered(b.into(), false)),
)
}
}
_ => (self.state, None),
},

@ -5,7 +5,7 @@ use crate::{
component::{Child, Component, Event, EventCtx, Label},
constant::{HEIGHT, WIDTH},
display::{
rect_fill,
self, rect_fill,
toif::{Toif, ToifFormat},
Font, Icon,
},
@ -17,7 +17,7 @@ use crate::{
use super::{
super::constant, common::display_center, theme, ButtonController, ButtonControllerMsg,
ButtonLayout, ButtonPos, CancelConfirmMsg,
ButtonLayout, ButtonPos, CancelConfirmMsg, LoaderMsg, ProgressLoader,
};
const AREA: Rect = constant::screen();
@ -34,6 +34,8 @@ const NOTIFICATION_FONT: Font = Font::NORMAL;
const NOTIFICATION_ICON: Icon = theme::ICON_WARNING;
const COINJOIN_CORNER: Point = AREA.top_right().ofs(Offset::new(-2, 2));
const HOLD_TO_LOCK_MS: u32 = 1000;
fn paint_default_image() {
theme::ICON_LOGO.draw(
TOP_CENTER + Offset::y(LOGO_ICON_TOP_MARGIN),
@ -43,6 +45,12 @@ fn paint_default_image() {
);
}
enum CurrentScreen {
EmptyAtStart,
Homescreen,
Loader,
}
pub struct Homescreen<T>
where
T: StringType,
@ -53,18 +61,31 @@ where
notification: Option<(T, u8)>,
/// Used for HTC functionality to lock device from homescreen
invisible_buttons: Child<ButtonController<T>>,
/// Holds the loader component
loader: Option<Child<ProgressLoader<T>>>,
/// Whether to show the loader or not
show_loader: bool,
/// Which screen is currently shown
current_screen: CurrentScreen,
}
impl<T> Homescreen<T>
where
T: StringType + Clone,
{
pub fn new(label: T, notification: Option<(T, u8)>) -> Self {
let invisible_btn_layout = ButtonLayout::htc_none_htc("".into(), "".into());
pub fn new(label: T, notification: Option<(T, u8)>, loader_description: Option<T>) -> Self {
// Buttons will not be visible, we only need both left and right to be existing
// so we can get the events from them.
let invisible_btn_layout = ButtonLayout::text_none_text("".into(), "".into());
let loader =
loader_description.map(|desc| Child::new(ProgressLoader::new(desc, HOLD_TO_LOCK_MS)));
Self {
label: Label::centered(label, theme::TEXT_BIG),
notification,
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
loader,
show_loader: false,
current_screen: CurrentScreen::EmptyAtStart,
}
}
@ -152,24 +173,64 @@ where
fn place(&mut self, bounds: Rect) -> Rect {
self.label.place(LABEL_AREA);
self.loader.place(AREA);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
Self::event_usb(self, ctx, event);
// HTC press of any button will lock the device
if let Some(ButtonControllerMsg::Triggered(..)) = self.invisible_buttons.event(ctx, event) {
return Some(());
// Only care about button and loader events when there is a possibility of
// locking the device
if let Some(self_loader) = &mut self.loader {
// When loader has completely grown, we can lock the device
if let Some(LoaderMsg::GrownCompletely) = self_loader.event(ctx, event) {
return Some(());
}
// Longer hold of any button will lock the device.
// Normal/quick presses and releases will show/hide the loader.
let button_event = self.invisible_buttons.event(ctx, event);
if let Some(ButtonControllerMsg::Pressed(..)) = button_event {
if !self.show_loader {
self.show_loader = true;
self_loader.mutate(ctx, |ctx, loader| {
loader.start(ctx);
ctx.request_paint();
});
}
}
if let Some(ButtonControllerMsg::Triggered(..)) = button_event {
self.show_loader = false;
self_loader.mutate(ctx, |ctx, loader| {
loader.stop(ctx);
ctx.request_paint();
});
}
}
None
}
fn paint(&mut self) {
// Painting the homescreen image first, as the notification and label
// should be "on top of it"
self.paint_homescreen_image();
self.paint_notification();
self.paint_label();
// Redraw the whole screen when the screen changes (loader vs homescreen)
if self.show_loader {
if !matches!(self.current_screen, CurrentScreen::Loader) {
display::clear();
self.current_screen = CurrentScreen::Loader;
}
self.loader.paint();
} else {
if !matches!(self.current_screen, CurrentScreen::Homescreen) {
display::clear();
self.current_screen = CurrentScreen::Homescreen;
}
// Painting the homescreen image first, as the notification and label
// should be "on top of it"
self.paint_homescreen_image();
self.paint_notification();
self.paint_label();
}
}
}

@ -3,14 +3,15 @@ use crate::{
time::{Duration, Instant},
ui::{
animation::Animation,
component::{Component, Event, EventCtx},
display::{self, Color, Font},
component::{Child, Component, Event, EventCtx},
constant,
display::{self, Color, Font, LOADER_MAX},
geometry::{Offset, Rect},
util::animation_disabled,
},
};
use super::theme;
use super::{theme, Progress};
pub const DEFAULT_DURATION_MS: u32 = 1000;
pub const SHRINKING_DURATION_MS: u32 = 500;
@ -186,10 +187,9 @@ where
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let now = Instant::now();
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.is_animating() {
let now = Instant::now();
if self.is_completely_grown(now) {
self.state = State::Grown;
ctx.request_paint();
@ -256,6 +256,93 @@ impl LoaderStyleSheet {
}
}
pub struct ProgressLoader<T>
where
T: StringType,
{
loader: Child<Progress<T>>,
duration_ms: u32,
start_time: Option<Instant>,
}
impl<T> ProgressLoader<T>
where
T: StringType + Clone,
{
const LOADER_FRAMES_DEFAULT: u32 = 20;
pub fn new(loader_description: T, duration_ms: u32) -> Self {
Self {
loader: Child::new(
Progress::new(false, loader_description).with_icon(theme::ICON_LOCK_SMALL),
),
duration_ms,
start_time: None,
}
}
pub fn start(&mut self, ctx: &mut EventCtx) {
self.start_time = Some(Instant::now());
self.loader.event(ctx, Event::Progress(0, ""));
self.loader.mutate(ctx, |ctx, loader| {
loader.request_paint(ctx);
});
ctx.request_anim_frame();
}
pub fn stop(&mut self, _ctx: &mut EventCtx) {
self.start_time = None;
}
fn is_animating(&self) -> bool {
self.start_time.is_some()
}
fn percentage(&self, now: Instant) -> u32 {
if let Some(start_time) = self.start_time {
let elapsed = now.saturating_duration_since(start_time);
let elapsed_ms = elapsed.to_millis();
(elapsed_ms * 100) / self.duration_ms
} else {
0
}
}
}
impl<T> Component for ProgressLoader<T>
where
T: StringType + Clone,
{
type Msg = LoaderMsg;
fn place(&mut self, bounds: Rect) -> Rect {
self.loader.place(constant::screen());
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
if self.is_animating() {
let now = Instant::now();
let percentage = self.percentage(now);
let new_loader_value = (percentage * LOADER_MAX as u32) / 100;
self.loader
.event(ctx, Event::Progress(new_loader_value as u16, ""));
// Returning only after the loader was fully painted
if percentage >= 100 {
return Some(LoaderMsg::GrownCompletely);
}
ctx.request_anim_frame();
}
}
None
}
fn paint(&mut self) {
self.loader.paint();
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]

@ -22,7 +22,7 @@ pub use input_methods::{
choice::{Choice, ChoiceFactory, ChoicePage},
choice_item::ChoiceItem,
};
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet, ProgressLoader};
pub use result::ResultScreen;
pub use welcome_screen::WelcomeScreen;

@ -10,7 +10,7 @@ use crate::{
Child, Component, Event, EventCtx, Label, Never, Pad,
},
constant,
display::{self, Font},
display::{self, Font, Icon, LOADER_MAX},
geometry::Rect,
util::animation_disabled,
},
@ -22,17 +22,21 @@ const BOTTOM_DESCRIPTION_MARGIN: i16 = 10;
const LOADER_Y_OFFSET_TITLE: i16 = -10;
const LOADER_Y_OFFSET_NO_TITLE: i16 = -20;
// Clippy was complaining about `very complex type used`
type UpdateDescriptionFn<T, Error> = fn(&str) -> Result<T, Error>;
pub struct Progress<T>
where
T: StringType,
{
title: Child<Label<T>>,
title: Option<Child<Label<T>>>,
value: u16,
loader_y_offset: i16,
indeterminate: bool,
description: Child<Paragraphs<Paragraph<T>>>,
description_pad: Pad,
update_description: fn(&str) -> Result<T, Error>,
update_description: Option<UpdateDescriptionFn<T, Error>>,
icon: Icon,
}
impl<T> Progress<T>
@ -41,14 +45,9 @@ where
{
const AREA: Rect = constant::screen();
pub fn new(
title: T,
indeterminate: bool,
description: T,
update_description: fn(&str) -> Result<T, Error>,
) -> Self {
pub fn new(indeterminate: bool, description: T) -> Self {
Self {
title: Child::new(Label::centered(title, theme::TEXT_BOLD)),
title: None,
value: 0,
loader_y_offset: 0,
indeterminate,
@ -56,9 +55,42 @@ where
Paragraph::new(&theme::TEXT_NORMAL, description).centered(),
)),
description_pad: Pad::with_background(theme::BG),
update_description,
update_description: None,
icon: theme::ICON_TICK_FAT,
}
}
pub fn with_title(mut self, title: T) -> Self {
self.title = Some(Child::new(Label::centered(title, theme::TEXT_BOLD)));
self
}
pub fn with_update_description(
mut self,
update_description: UpdateDescriptionFn<T, Error>,
) -> Self {
self.update_description = Some(update_description);
self
}
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icon = icon;
self
}
pub fn request_paint(&self, ctx: &mut EventCtx) {
if !animation_disabled() {
ctx.request_paint();
}
}
pub fn value(&self) -> u16 {
self.value
}
pub fn reached_max_value(&self) -> bool {
self.value >= LOADER_MAX
}
}
impl<T> Component for Progress<T>
@ -78,11 +110,16 @@ where
.filter(|c| *c == '\n')
.count() as i16;
let (title, rest, loader_y_offset) = if self.title.inner().text().as_ref().is_empty() {
(Rect::zero(), Self::AREA, LOADER_Y_OFFSET_NO_TITLE)
let no_title_case = (Rect::zero(), Self::AREA, LOADER_Y_OFFSET_NO_TITLE);
let (title, rest, loader_y_offset) = if let Some(self_title) = &self.title {
if !self_title.inner().text().as_ref().is_empty() {
let (title, rest) = Self::AREA.split_top(self_title.inner().max_size().y);
(title, rest, LOADER_Y_OFFSET_TITLE)
} else {
no_title_case
}
} else {
let (title, rest) = Self::AREA.split_top(self.title.inner().max_size().y);
(title, rest, LOADER_Y_OFFSET_TITLE)
no_title_case
};
let (_loader, description) = rest.split_bottom(
@ -96,18 +133,21 @@ where
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.title.event(ctx, event);
self.description.event(ctx, event);
if let Event::Progress(new_value, new_description) = event {
if mem::replace(&mut self.value, new_value) != new_value {
if !animation_disabled() {
ctx.request_paint();
}
self.request_paint(ctx);
}
if let Some(update_description) = self.update_description {
self.description.mutate(ctx, |ctx, para| {
// NOTE: not doing any change for empty new descriptions
// (currently, there is no use-case for deleting the description)
if !new_description.is_empty()
&& para.inner_mut().content().as_ref() != new_description
{
let new_description = unwrap!((self.update_description)(new_description));
let new_description = unwrap!((update_description)(new_description));
para.inner_mut().update(new_description);
para.change_page(0); // Recompute bounding box.
ctx.request_paint();
@ -135,7 +175,7 @@ where
self.loader_y_offset,
theme::FG,
theme::BG,
Some((theme::ICON_TICK_FAT, theme::FG)),
Some((self.icon, theme::FG)),
);
}
self.description_pad.paint();

@ -1523,12 +1523,11 @@ extern "C" fn new_show_progress(n_args: usize, args: *const Obj, kwargs: *mut Ma
// Description updates are received as &str and we need to provide a way to
// convert them to StrBuffer.
let obj = LayoutObj::new(Progress::new(
title,
indeterminate,
description,
StrBuffer::alloc,
))?;
let obj = LayoutObj::new(
Progress::new(indeterminate, description)
.with_title(title)
.with_update_description(StrBuffer::alloc),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -1567,9 +1566,11 @@ extern "C" fn new_show_homescreen(n_args: usize, args: *const Obj, kwargs: *mut
kwargs.get(Qstr::MP_QSTR_notification)?.try_into_option()?;
let notification_level: u8 = kwargs.get_or(Qstr::MP_QSTR_notification_level, 0)?;
let skip_first_paint: bool = kwargs.get(Qstr::MP_QSTR_skip_first_paint)?.try_into()?;
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))?;
let loader_description = hold.then_some("Locking the device...".into());
let obj = LayoutObj::new(Homescreen::new(label, notification, loader_description))?;
if skip_first_paint {
obj.skip_first_paint();
}

@ -91,6 +91,7 @@ include_icon!(
include_icon!(ICON_DEVICE_NAME, "model_tr/res/device_name.toif"); // 116*18
include_icon!(ICON_EYE, "model_tr/res/eye_round.toif"); // 12*7
include_icon!(ICON_LOCK, "model_tr/res/lock.toif"); // 10*10
include_icon!(ICON_LOCK_SMALL, "model_tr/res/lock_small.toif"); // 6*7
include_icon!(ICON_LOGO, "model_tr/res/logo_22_33.toif"); // 22*33
include_icon!(ICON_LOGO_EMPTY, "model_tr/res/logo_22_33_empty.toif");
include_icon!(

@ -45,7 +45,7 @@ impl Trezor {
Box::new(|_, m: protos::EthereumMessageSignature| {
let signature = m.signature();
if signature.len() != 65 {
return Err(Error::MalformedSignature);
return Err(Error::MalformedSignature)
}
let r = signature[0..32].try_into().unwrap();
let s = signature[32..64].try_into().unwrap();

Loading…
Cancel
Save