diff --git a/core/.changelog.d/3863.added b/core/.changelog.d/3863.added new file mode 100644 index 0000000000..20f9d3edf7 --- /dev/null +++ b/core/.changelog.d/3863.added @@ -0,0 +1 @@ +Show last typed PIN number for short period of time diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/pin.rs index 9f25bc49dd..59b0e5edee 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/pin.rs @@ -49,6 +49,8 @@ const HEADER_PADDING: Insets = Insets::new( HEADER_PADDING_SIDE, ); +const LAST_DIGIT_TIMEOUT_S: u32 = 1; + #[derive(Default, Clone)] struct AttachAnimation { pub attach_top: bool, @@ -260,6 +262,7 @@ pub struct PinKeyboard<'a> { attach_animation: AttachAnimation, close_animation: CloseAnimation, close_confirm: bool, + timeout_timer: Timer, } impl<'a> PinKeyboard<'a> { @@ -299,6 +302,7 @@ impl<'a> PinKeyboard<'a> { attach_animation: AttachAnimation::default(), close_animation: CloseAnimation::default(), close_confirm: false, + timeout_timer: Timer::new(), } } @@ -425,6 +429,12 @@ impl Component for PinKeyboard<'_> { self.minor_prompt.request_complete_repaint(ctx); ctx.request_paint(); } + // Timeout for showing the last digit. + Event::Timer(_) if self.timeout_timer.expire(event) => { + self.textbox.display_style = DisplayStyle::Dots; + self.textbox.request_complete_repaint(ctx); + ctx.request_paint(); + } _ => {} } @@ -470,6 +480,11 @@ impl Component for PinKeyboard<'_> { self.textbox.push(ctx, text); }); self.pin_modified(ctx); + self.timeout_timer + .start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S)); + self.textbox.display_style = DisplayStyle::LastDigit; + self.textbox.request_complete_repaint(ctx); + ctx.request_paint(); return None; } } @@ -524,7 +539,15 @@ struct PinDots { pad: Pad, style: TextStyle, digits: ShortString, - display_digits: bool, + display_style: DisplayStyle, +} + +#[derive(PartialEq, Debug, Copy, Clone)] +#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] +enum DisplayStyle { + Dots, + Digits, + LastDigit, } impl PinDots { @@ -538,7 +561,7 @@ impl PinDots { pad: Pad::with_background(style.background_color), style, digits: ShortString::new(), - display_digits: false, + display_style: DisplayStyle::Dots, } } @@ -600,7 +623,7 @@ impl PinDots { } } - fn render_dots<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { + fn render_dots<'s>(&self, last_digit: bool, area: Rect, target: &mut impl Renderer<'s>) { let mut cursor = area.left_center(); let digits = self.digits.len(); @@ -634,13 +657,28 @@ impl PinDots { } // Draw a dot for each PIN digit. - for _ in digit_idx..dots_visible { + for _ in digit_idx..dots_visible - 1 { shape::ToifImage::new(cursor, theme::ICON_PIN_BULLET.toif) .with_align(Alignment2D::CENTER_LEFT) .with_fg(self.style.text_color) .render(target); cursor.x += step; } + + if last_digit && digits > 0 { + let last = &self.digits[(digits - 1)..digits]; + cursor.y = area.left_center().y + (Font::MONO.visible_text_height("1") / 2); + shape::Text::new(cursor, last) + .with_align(Alignment::Start) + .with_font(Font::MONO) + .with_fg(self.style.text_color) + .render(target); + } else { + shape::ToifImage::new(cursor, theme::ICON_PIN_BULLET.toif) + .with_align(Alignment2D::CENTER_LEFT) + .with_fg(self.style.text_color) + .render(target); + } } } @@ -657,14 +695,15 @@ impl Component for PinDots { match event { Event::Touch(TouchEvent::TouchStart(pos)) => { if self.area.contains(pos) { - self.display_digits = true; + self.display_style = DisplayStyle::Digits; self.pad.clear(); ctx.request_paint(); }; None } Event::Touch(TouchEvent::TouchEnd(_)) => { - if mem::replace(&mut self.display_digits, false) { + if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Digits + { self.pad.clear(); ctx.request_paint(); }; @@ -677,10 +716,10 @@ impl Component for PinDots { fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { let dot_area = self.area.inset(HEADER_PADDING); self.pad.render(target); - if self.display_digits { - self.render_digits(dot_area, target) - } else { - self.render_dots(dot_area, target) + match self.display_style { + DisplayStyle::Digits => self.render_digits(dot_area, target), + DisplayStyle::Dots => self.render_dots(false, dot_area, target), + DisplayStyle::LastDigit => self.render_dots(true, dot_area, target), } } } @@ -699,8 +738,9 @@ impl crate::trace::Trace for PinKeyboard<'_> { }); } } + let display_style = uformat!("{:?}", self.textbox.display_style); t.string("digits_order", digits_order.as_str().into()); t.string("pin", self.textbox.pin().into()); - t.bool("display_digits", self.textbox.display_digits); + t.string("display_style", display_style.as_str().into()); } } diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs index 5336282a80..9899ec1479 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs @@ -1,9 +1,12 @@ use crate::{ strutil::{ShortString, TString}, + time::Duration, translations::TR, trezorhal::random, ui::{ - component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx}, + component::{ + text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx, Timer, + }, display::{Font, Icon}, geometry::Rect, shape::Renderer, @@ -52,6 +55,8 @@ const EMPTY_PIN_STR: &str = "_"; const CHOICE_LENGTH: usize = 13; const NUMBER_START_INDEX: usize = 3; +const LAST_DIGIT_TIMEOUT_S: u32 = 1; + const CHOICES: [PinChoice; CHOICE_LENGTH] = [ // DELETE should be triggerable without release (after long-press) PinChoice::new( @@ -140,6 +145,7 @@ pub struct PinEntry<'a> { show_real_pin: bool, show_last_digit: bool, textbox: TextBox, + timeout_timer: Timer, } impl<'a> PinEntry<'a> { @@ -179,6 +185,7 @@ impl<'a> PinEntry<'a> { show_real_pin: false, show_last_digit: false, textbox: TextBox::empty(MAX_PIN_LENGTH), + timeout_timer: Timer::new(), } } @@ -262,17 +269,22 @@ impl Component for PinEntry<'_> { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - // Any non-timer event when showing real PIN should hide it - // Same with showing last digit - if !matches!(event, Event::Timer(_)) { - if self.show_real_pin { + match event { + // Timeout for showing the last digit. + Event::Timer(_) if self.timeout_timer.expire(event) => { + if self.show_last_digit { + self.show_last_digit = false; + self.update(ctx) + } + } + // Other timers are ignored. + Event::Timer(_) => {} + // Any non-timer event when showing real PIN should hide it + _ if self.show_real_pin => { self.show_real_pin = false; - self.update(ctx) - } - if self.show_last_digit { - self.show_last_digit = false; - self.update(ctx) + self.update(ctx); } + _ => {} } // Any button event will show the "real" prompt @@ -308,6 +320,8 @@ impl Component for PinEntry<'_> { self.choice_page .set_page_counter(ctx, get_random_digit_position(), true); self.show_last_digit = true; + self.timeout_timer + .start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S)); self.update(ctx); } _ => {} diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs index 6f4523b6ef..a430e279d3 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs @@ -44,6 +44,8 @@ const HEADER_PADDING: Insets = Insets::new( HEADER_PADDING_SIDE, ); +const LAST_DIGIT_TIMEOUT_S: u32 = 1; + pub struct PinKeyboard<'a> { allow_cancel: bool, major_prompt: Child>, @@ -56,6 +58,7 @@ pub struct PinKeyboard<'a> { confirm_btn: Child