From 02b2bf99e341f1e5d08ce32557d752471f782045 Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Mon, 18 Nov 2024 18:28:54 +0100 Subject: [PATCH 01/12] chore(core): show the last passphrase character for a while --- core/.changelog.d/3959.added | 1 + .../component/keyboard/passphrase.rs | 111 ++++++++++++++-- .../rust/src/ui/model_mercury/theme/mod.rs | 4 + .../component/input_methods/passphrase.rs | 32 +++-- .../model_tt/component/keyboard/passphrase.rs | 123 ++++++++++++++++-- core/embed/rust/src/ui/model_tt/theme/mod.rs | 4 + 6 files changed, 244 insertions(+), 31 deletions(-) create mode 100644 core/.changelog.d/3959.added diff --git a/core/.changelog.d/3959.added b/core/.changelog.d/3959.added new file mode 100644 index 00000000000..04bb75986c9 --- /dev/null +++ b/core/.changelog.d/3959.added @@ -0,0 +1 @@ +Show last typed passphrase character for short period of time diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs index 6b6e5ee5809..1a0a70bf0c3 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs @@ -1,12 +1,14 @@ use crate::{ strutil::{ShortString, TString}, + time::Duration, translations::TR, ui::{ component::{ base::ComponentExt, swipe_detect::SwipeConfig, text::common::TextBox, Component, Event, - EventCtx, Label, Maybe, Never, Swipe, + EventCtx, Label, Maybe, Never, Swipe, Timer, }, display, + event::TouchEvent, geometry::{Alignment, Direction, Grid, Insets, Offset, Rect}, model_mercury::{ component::{ @@ -22,7 +24,7 @@ use crate::{ }, }; -use core::cell::Cell; +use core::{cell::Cell, mem}; use num_traits::ToPrimitive; pub enum PassphraseKeyboardMsg { @@ -30,6 +32,14 @@ pub enum PassphraseKeyboardMsg { Cancelled, } +#[derive(PartialEq, Debug, Copy, Clone)] +#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] +enum DisplayStyle { + Dots, + Chars, + LastChar, +} + /// Enum keeping track of which keyboard is shown and which comes next. Keep the /// number of values and the constant PAGE_COUNT in synch. #[repr(u32)] @@ -105,6 +115,8 @@ const MAX_LENGTH: usize = 50; const CONFIRM_BTN_INSETS: Insets = Insets::new(5, 0, 5, 0); const CONFIRM_EMPTY_BTN_MARGIN_RIGHT: i16 = 7; const CONFIRM_EMPTY_BTN_INSETS: Insets = Insets::new(5, CONFIRM_EMPTY_BTN_MARGIN_RIGHT, 5, 0); +const INPUT_INSETS: Insets = Insets::new(10, 2, 10, 4); +const LAST_DIGIT_TIMEOUT_S: u32 = 1; impl PassphraseKeyboard { pub fn new() -> Self { @@ -272,7 +284,6 @@ impl Component for PassphraseKeyboard { fn place(&mut self, bounds: Rect) -> Rect { const CONFIRM_BTN_WIDTH: i16 = 78; const CONFIRM_EMPTY_BTN_WIDTH: i16 = 32; - const INPUT_INSETS: Insets = Insets::new(10, 2, 10, 4); let bounds = bounds.inset(theme::borders()); let (top_area, keypad_area) = @@ -284,7 +295,7 @@ impl Component for PassphraseKeyboard { .1; let top_area = top_area.inset(INPUT_INSETS); - let input_area = input_area.inset(INPUT_INSETS); + // let input_area = input_area.inset(INPUT_INSETS); let confirm_btn_area = confirm_btn_area.inset(CONFIRM_BTN_INSETS); let confirm_empty_btn_area = confirm_empty_btn_area.inset(CONFIRM_EMPTY_BTN_INSETS); @@ -323,8 +334,14 @@ impl Component for PassphraseKeyboard { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { if self.input.multi_tap.timeout_event(event) { self.input.multi_tap.clear_pending_state(ctx); + self.input + .last_char_timer + .start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S)); return None; } + + self.input.event(ctx, event); + if let Some(swipe) = self.page_swipe.event(ctx, event) { // We have detected a horizontal swipe. Change the keyboard page. self.on_page_change(ctx, swipe); @@ -383,6 +400,17 @@ impl Component for PassphraseKeyboard { let edit = text.map(|c| self.input.multi_tap.click_key(ctx, key, c)); self.input.textbox.apply(ctx, edit); self.after_edit(ctx); + if text.len() == 1 { + // If the key has just one character, it is immediately applied and the last + // digit timer should be started + self.input + .last_char_timer + .start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S)); + } else { + // multi tap timer is runnig, the last digit timer should be stopped + self.input.last_char_timer.stop(); + } + self.input.display_style = DisplayStyle::LastChar; return None; } } @@ -416,14 +444,46 @@ struct Input { area: Rect, textbox: TextBox, multi_tap: MultiTapKeyboard, + display_style: DisplayStyle, + last_char_timer: Timer, } impl Input { + const TWITCH: i16 = 4; + fn new() -> Self { Self { area: Rect::zero(), textbox: TextBox::empty(MAX_LENGTH), multi_tap: MultiTapKeyboard::new(), + display_style: DisplayStyle::LastChar, + last_char_timer: Timer::new(), + } + } + + fn apply_display_style(&self, passphrase: &ShortString) -> ShortString { + if passphrase.len() == 0 { + ShortString::new() + } else { + match self.display_style { + DisplayStyle::Dots => { + let mut dots = ShortString::new(); + for _ in 0..(passphrase.len()) { + dots.push('*').unwrap(); + } + dots + } + DisplayStyle::Chars => passphrase.clone(), + DisplayStyle::LastChar => { + let mut dots = ShortString::new(); + for _ in 0..(passphrase.len() - 1) { + dots.push('*').unwrap(); + } + // This should not fail because the passphrase is not empty. + dots.push(passphrase.chars().last().unwrap()).unwrap(); + dots + } + } } } } @@ -436,26 +496,55 @@ impl Component for Input { self.area } - fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match event { + Event::Timer(_) if self.last_char_timer.expire(event) => { + self.display_style = DisplayStyle::Dots; + ctx.request_paint(); + } + Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => { + self.display_style = DisplayStyle::Chars; + ctx.request_paint(); + } + Event::Touch(TouchEvent::TouchEnd(_)) => { + if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Chars + { + ctx.request_paint(); + }; + } + _ => {} + } None } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { - let style = theme::label_keyboard(); + let style = theme::label_keyboard_mono(); + let text_area = self.area.inset(INPUT_INSETS); - let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height()) + let mut text_baseline = text_area.top_left() + Offset::y(style.text_font.text_height()) - Offset::y(style.text_font.text_baseline()); let text = self.textbox.content(); - shape::Bar::new(self.area).with_bg(theme::BG).render(target); + shape::Bar::new(text_area).with_bg(theme::BG).render(target); // Find out how much text can fit into the textbox. // Accounting for the pending marker, which draws itself one pixel longer than // the last character - let available_area_width = self.area.width() - 1; - let text_to_display = - long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width); + let available_area_width = text_area.width() - 1; + let truncated = + long_line_content_with_ellipsis(text, "", style.text_font, available_area_width); + + // Jiggle hidden passphrase when overflowed. + if text.len() > truncated.len() + && text.len() % 2 == 0 + && (self.display_style == DisplayStyle::Dots + || self.display_style == DisplayStyle::LastChar) + { + text_baseline.x += Self::TWITCH; + } + + let text_to_display = self.apply_display_style(&truncated); shape::Text::new(text_baseline, &text_to_display) .with_font(style.text_font) diff --git a/core/embed/rust/src/ui/model_mercury/theme/mod.rs b/core/embed/rust/src/ui/model_mercury/theme/mod.rs index 0c8580f54a5..11b49b75ec2 100644 --- a/core/embed/rust/src/ui/model_mercury/theme/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/theme/mod.rs @@ -136,6 +136,10 @@ pub const fn label_keyboard() -> TextStyle { TextStyle::new(Font::DEMIBOLD, GREY_EXTRA_LIGHT, BG, GREY_LIGHT, GREY_LIGHT) } +pub const fn label_keyboard_mono() -> TextStyle { + TextStyle::new(Font::MONO, GREY_EXTRA_LIGHT, BG, GREY_LIGHT, GREY_LIGHT) +} + pub const fn label_keyboard_prompt() -> TextStyle { TextStyle::new(Font::DEMIBOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT) } diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs index b0335b75733..31f2760a39e 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.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::Icon, geometry::Rect, shape::Renderer, @@ -43,6 +46,8 @@ const DIGITS_INDEX: usize = 5; const SPECIAL_INDEX: usize = 6; const SPACE_INDEX: usize = 7; +const LAST_DIGIT_TIMEOUT_S: u32 = 1; + #[derive(Clone)] struct MenuItem { text: TString<'static>, @@ -273,6 +278,7 @@ pub struct PassphraseEntry { show_last_digit: bool, textbox: TextBox, current_category: ChoiceCategory, + last_char_timer: Timer, } impl PassphraseEntry { @@ -286,6 +292,7 @@ impl PassphraseEntry { show_last_digit: false, textbox: TextBox::empty(MAX_PASSPHRASE_LENGTH), current_category: ChoiceCategory::Menu, + last_char_timer: Timer::new(), } } @@ -383,17 +390,22 @@ impl Component for PassphraseEntry { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - // Any non-timer event when showing real passphrase should hide it - // Same with showing last digit - if !matches!(event, Event::Timer(_)) { - if self.show_plain_passphrase { - self.show_plain_passphrase = false; - self.update_passphrase_dots(ctx); + match event { + // Timeout for showing the last digit. + Event::Timer(_) if self.last_char_timer.expire(event) => { + if self.show_last_digit { + self.show_last_digit = false; + self.update_passphrase_dots(ctx); + } } - if self.show_last_digit { - self.show_last_digit = false; + // Other timers are ignored. + Event::Timer(_) => {} + // Any non-timer event when showing plain passphrase should hide it + _ if self.show_plain_passphrase => { + self.show_plain_passphrase = false; self.update_passphrase_dots(ctx); } + _ => {} } if let Some((action, long_press)) = self.choice_page.event(ctx, event) { @@ -437,6 +449,8 @@ impl Component for PassphraseEntry { PassphraseAction::Character(ch) if !self.is_full() => { self.append_char(ctx, ch); self.show_last_digit = true; + self.last_char_timer + .start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S)); self.update_passphrase_dots(ctx); self.randomize_category_position(ctx); ctx.request_paint(); diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index e66d01871d9..24f81a1e658 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -1,10 +1,13 @@ use crate::{ - strutil::TString, + strutil::{ShortString, TString}, + time::Duration, ui::{ component::{ base::ComponentExt, text::common::TextBox, Child, Component, Event, EventCtx, Never, + Timer, }, display, + event::TouchEvent, geometry::{Grid, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg}, @@ -12,13 +15,11 @@ use crate::{ swipe::{Swipe, SwipeDirection}, theme, ScrollBar, }, - shape, - shape::Renderer, + shape::{self, Renderer}, util::long_line_content_with_ellipsis, }, }; - -use core::cell::Cell; +use core::{cell::Cell, mem}; #[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum PassphraseKeyboardMsg { @@ -26,6 +27,14 @@ pub enum PassphraseKeyboardMsg { Cancelled, } +#[derive(PartialEq, Debug, Copy, Clone)] +#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] +enum DisplayStyle { + Dots, + Chars, + LastChar, +} + pub struct PassphraseKeyboard { page_swipe: Swipe, input: Child, @@ -50,6 +59,8 @@ const KEYBOARD: [[&str; KEY_COUNT]; PAGE_COUNT] = [ const MAX_LENGTH: usize = 50; const INPUT_AREA_HEIGHT: i16 = ScrollBar::DOT_SIZE + 9; +const LAST_DIGIT_TIMEOUT_S: u32 = 1; + impl PassphraseKeyboard { pub fn new() -> Self { Self { @@ -189,7 +200,7 @@ impl Component for PassphraseKeyboard { let (input_area, key_grid_area) = bounds.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING); - let (input_area, scroll_area) = input_area.split_bottom(INPUT_AREA_HEIGHT); + let (_, scroll_area) = input_area.split_bottom(INPUT_AREA_HEIGHT); let (scroll_area, _) = scroll_area.split_top(ScrollBar::DOT_SIZE); let key_grid = Grid::new(key_grid_area, 4, 3).with_spacing(theme::BUTTON_SPACING); @@ -231,8 +242,14 @@ impl Component for PassphraseKeyboard { } }); if multitap_timeout { + // the character has been added, show it for a bit and then hide it + self.input + .mutate(ctx, |ctx, t| t.start_timer(ctx, LAST_DIGIT_TIMEOUT_S)); return None; } + + self.input.event(ctx, event); + if let Some(swipe) = self.page_swipe.event(ctx, event) { // We have detected a horizontal swipe. Change the keyboard page. self.on_page_swipe(ctx, swipe); @@ -288,6 +305,17 @@ impl Component for PassphraseKeyboard { i.textbox.apply(ctx, edit); }); self.after_edit(ctx); + if text.len() == 1 { + // If the key has just one character, it is immediately applied and the last + // digit timer should be started + self.input + .mutate(ctx, |ctx, t| t.start_timer(ctx, LAST_DIGIT_TIMEOUT_S)); + } else { + // multi tap timer is runnig, the last digit timer should be stopped + self.input.mutate(ctx, |_ctx, t| t.stop_timer()); + } + self.input + .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::LastChar)); return None; } } @@ -313,14 +341,59 @@ struct Input { area: Rect, textbox: TextBox, multi_tap: MultiTapKeyboard, + display_style: DisplayStyle, + last_char_timer: Timer, } impl Input { + const TWITCH: i16 = 4; + fn new() -> Self { Self { area: Rect::zero(), textbox: TextBox::empty(MAX_LENGTH), multi_tap: MultiTapKeyboard::new(), + display_style: DisplayStyle::LastChar, + last_char_timer: Timer::new(), + } + } + + fn set_display_style(&mut self, display_style: DisplayStyle) { + self.display_style = display_style; + } + + fn start_timer(&mut self, ctx: &mut EventCtx, timeout_ms: u32) { + self.last_char_timer + .start(ctx, Duration::from_secs(timeout_ms)); + } + + fn stop_timer(&mut self) { + self.last_char_timer.stop(); + } + + fn apply_display_style(&self, passphrase: &ShortString) -> ShortString { + if passphrase.len() == 0 { + ShortString::new() + } else { + match self.display_style { + DisplayStyle::Dots => { + let mut dots = ShortString::new(); + for _ in 0..(passphrase.len()) { + dots.push('*').unwrap(); + } + dots + } + DisplayStyle::Chars => passphrase.clone(), + DisplayStyle::LastChar => { + let mut dots = ShortString::new(); + for _ in 0..(passphrase.len() - 1) { + dots.push('*').unwrap(); + } + // This should not fail because the passphrase is not empty. + dots.push(passphrase.chars().last().unwrap()).unwrap(); + dots + } + } } } } @@ -333,14 +406,31 @@ impl Component for Input { self.area } - fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + match event { + Event::Timer(_) if self.last_char_timer.expire(event) => { + self.display_style = DisplayStyle::Dots; + ctx.request_paint(); + } + Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => { + self.display_style = DisplayStyle::Chars; + ctx.request_paint(); + } + Event::Touch(TouchEvent::TouchEnd(_)) => { + if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Chars + { + ctx.request_paint(); + }; + } + _ => {} + } None } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { - let style = theme::label_keyboard(); + let style = theme::label_keyboard_mono(); - let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height()) + let mut text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height()) - Offset::y(style.text_font.text_baseline()); let text = self.textbox.content(); @@ -351,8 +441,19 @@ impl Component for Input { // Accounting for the pending marker, which draws itself one pixel longer than // the last character let available_area_width = self.area.width() - 1; - let text_to_display = - long_line_content_with_ellipsis(text, "...", style.text_font, available_area_width); + let truncated = + long_line_content_with_ellipsis(text, "", style.text_font, available_area_width); + + // Jiggle hidden passphrase when overflowed. + if text.len() > truncated.len() + && text.len() % 2 == 0 + && (self.display_style == DisplayStyle::Dots + || self.display_style == DisplayStyle::LastChar) + { + text_baseline.x += Self::TWITCH; + } + + let text_to_display = self.apply_display_style(&truncated); shape::Text::new(text_baseline, &text_to_display) .with_font(style.text_font) diff --git a/core/embed/rust/src/ui/model_tt/theme/mod.rs b/core/embed/rust/src/ui/model_tt/theme/mod.rs index da8e73d8fe0..651cc22e2d2 100644 --- a/core/embed/rust/src/ui/model_tt/theme/mod.rs +++ b/core/embed/rust/src/ui/model_tt/theme/mod.rs @@ -120,6 +120,10 @@ pub const fn label_keyboard() -> TextStyle { TextStyle::new(Font::DEMIBOLD, OFF_WHITE, BG, GREY_LIGHT, GREY_LIGHT) } +pub const fn label_keyboard_mono() -> TextStyle { + TextStyle::new(Font::MONO, OFF_WHITE, BG, GREY_LIGHT, GREY_LIGHT) +} + pub const fn label_keyboard_prompt() -> TextStyle { TextStyle::new(Font::DEMIBOLD, GREY_LIGHT, BG, GREY_LIGHT, GREY_LIGHT) } From 4102127a66b75dd0656f533ef28280d8dbc32f74 Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Mon, 25 Nov 2024 20:20:46 +0100 Subject: [PATCH 02/12] fixup! chore(core): show the last passphrase character for a while --- .../rust/src/ui/model_mercury/component/keyboard/passphrase.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs index 1a0a70bf0c3..6c8d545bda5 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs @@ -295,7 +295,6 @@ impl Component for PassphraseKeyboard { .1; let top_area = top_area.inset(INPUT_INSETS); - // let input_area = input_area.inset(INPUT_INSETS); let confirm_btn_area = confirm_btn_area.inset(CONFIRM_BTN_INSETS); let confirm_empty_btn_area = confirm_empty_btn_area.inset(CONFIRM_EMPTY_BTN_INSETS); From 2018df17ec9df86baf697366fd6cb3e54730d17a Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Mon, 25 Nov 2024 20:39:02 +0100 Subject: [PATCH 03/12] fixup! chore(core): show the last passphrase character for a while --- .../src/ui/model_tr/component/input_methods/passphrase.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs index 31f2760a39e..440a5d87021 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs @@ -287,7 +287,9 @@ impl PassphraseEntry { choice_page: ChoicePage::new(ChoiceFactoryPassphrase::new(ChoiceCategory::Menu, true)) .with_carousel(true) .with_initial_page_counter(random_menu_position()), - passphrase_dots: Child::new(ChangingTextLine::center_mono("", MAX_PASSPHRASE_LENGTH)), + passphrase_dots: Child::new( + ChangingTextLine::center_mono("", MAX_PASSPHRASE_LENGTH).without_ellipsis(), + ), show_plain_passphrase: false, show_last_digit: false, textbox: TextBox::empty(MAX_PASSPHRASE_LENGTH), From 348e04d093ade482e204474e2dad199543e3eafd Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Wed, 27 Nov 2024 19:01:32 +0100 Subject: [PATCH 04/12] fixup! chore(core): show the last passphrase character for a while --- .../component/keyboard/passphrase.rs | 188 +++++++++++------ .../model_tt/component/keyboard/passphrase.rs | 196 ++++++++++++------ 2 files changed, 262 insertions(+), 122 deletions(-) diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs index 6c8d545bda5..503fac9f69a 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs @@ -9,7 +9,7 @@ use crate::{ }, display, event::TouchEvent, - geometry::{Alignment, Direction, Grid, Insets, Offset, Rect}, + geometry::{Alignment, Alignment2D, Direction, Grid, Insets, Offset, Rect}, model_mercury::{ component::{ button::{Button, ButtonContent, ButtonMsg}, @@ -449,6 +449,7 @@ struct Input { impl Input { const TWITCH: i16 = 4; + const X_STEP: i16 = 13; fn new() -> Self { Self { @@ -460,28 +461,125 @@ impl Input { } } - fn apply_display_style(&self, passphrase: &ShortString) -> ShortString { - if passphrase.len() == 0 { - ShortString::new() - } else { - match self.display_style { - DisplayStyle::Dots => { - let mut dots = ShortString::new(); - for _ in 0..(passphrase.len()) { - dots.push('*').unwrap(); - } - dots - } - DisplayStyle::Chars => passphrase.clone(), - DisplayStyle::LastChar => { - let mut dots = ShortString::new(); - for _ in 0..(passphrase.len() - 1) { - dots.push('*').unwrap(); - } - // This should not fail because the passphrase is not empty. - dots.push(passphrase.chars().last().unwrap()).unwrap(); - dots + fn render_chars<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { + let style = theme::label_keyboard_mono(); + let mut text_baseline = area.top_left() + Offset::y(style.text_font.text_height()); + let chars = self.textbox.content().len(); + + if chars > 0 { + // Find out how much text can fit into the textbox. + // Accounting for the pending marker, which draws itself one extra pixel + let available_area_width = area.width() - 1; + let truncated = long_line_content_with_ellipsis( + self.textbox.content(), + "", + style.text_font, + available_area_width, + ); + + // Jiggle hidden passphrase when overflowed. + if chars > truncated.len() + && chars % 2 == 0 + && (self.display_style == DisplayStyle::Dots + || self.display_style == DisplayStyle::LastChar) + { + text_baseline.x += Self::TWITCH; + } + + // Paint the visible passphrase. + shape::Text::new(text_baseline, &truncated) + .with_font(style.text_font) + .with_fg(style.text_color) + .render(target); + + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() { + render_pending_marker( + target, + text_baseline, + &truncated, + style.text_font, + style.text_color, + ); + } + } + } + + fn render_dots<'s>(&self, last_char: bool, area: Rect, target: &mut impl Renderer<'s>) { + let style = theme::label_keyboard_mono(); + let bullet = theme::ICON_PIN_BULLET; + let mut cursor = area.left_center(); + let all_chars = self.textbox.content().len(); + + if all_chars > 0 { + // Find out how much text can fit into the textbox. + // Accounting for the pending marker, which draws itself one extra pixel + let truncated = long_line_content_with_ellipsis( + self.textbox.content(), + "", + style.text_font, + area.width() - 1, + ); + let visible_chars = truncated.len(); + + // Jiggle when overflowed. + if all_chars > visible_chars + && all_chars % 2 == 0 + && (self.display_style == DisplayStyle::Dots + || self.display_style == DisplayStyle::LastChar) + { + cursor.x += Self::TWITCH; + } + + let mut char_idx = 0; + // Small leftmost dot. + if all_chars > visible_chars + 1 { + shape::ToifImage::new(cursor, theme::DOT_SMALL.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(theme::GREY) + .render(target); + cursor.x += Self::X_STEP; + char_idx += 1; + } + // Greyed out dot. + if all_chars > visible_chars { + shape::ToifImage::new(cursor, theme::DOT_SMALL.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(style.text_color) + .render(target); + cursor.x += Self::X_STEP; + char_idx += 1; + } + // Classical dot(s) + for _ in char_idx..(visible_chars - 1) { + shape::ToifImage::new(cursor, bullet) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(style.text_color) + .render(target); + cursor.x += Self::X_STEP; + } + + if last_char { + // Adapt y position for the character + cursor.y = area.top_left().y + style.text_font.text_height(); + // This should not fail because all_chars > 0 + let last = &self.textbox.content()[(all_chars - 1)..all_chars]; + // Paint the last character + shape::Text::new(cursor, last) + .with_align(Alignment::Start) + .with_font(style.text_font) + .with_fg(style.text_color) + .render(target); + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() { + render_pending_marker(target, cursor, last, style.text_font, style.text_color); } + } else { + // Last classical dot + shape::ToifImage::new(cursor, bullet) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(style.text_color) + .render(target); } } } @@ -517,48 +615,18 @@ impl Component for Input { } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { - let style = theme::label_keyboard_mono(); let text_area = self.area.inset(INPUT_INSETS); - let mut text_baseline = text_area.top_left() + Offset::y(style.text_font.text_height()) - - Offset::y(style.text_font.text_baseline()); - - let text = self.textbox.content(); - + // Paint the background shape::Bar::new(text_area).with_bg(theme::BG).render(target); - // Find out how much text can fit into the textbox. - // Accounting for the pending marker, which draws itself one pixel longer than - // the last character - let available_area_width = text_area.width() - 1; - let truncated = - long_line_content_with_ellipsis(text, "", style.text_font, available_area_width); - - // Jiggle hidden passphrase when overflowed. - if text.len() > truncated.len() - && text.len() % 2 == 0 - && (self.display_style == DisplayStyle::Dots - || self.display_style == DisplayStyle::LastChar) - { - text_baseline.x += Self::TWITCH; - } - - let text_to_display = self.apply_display_style(&truncated); - - shape::Text::new(text_baseline, &text_to_display) - .with_font(style.text_font) - .with_fg(style.text_color) - .render(target); - - // Paint the pending marker. - if self.multi_tap.pending_key().is_some() { - render_pending_marker( - target, - text_baseline, - &text_to_display, - style.text_font, - style.text_color, - ); + // Paint the passphrase + if !self.textbox.content().is_empty() { + match self.display_style { + DisplayStyle::Chars => self.render_chars(text_area, target), + DisplayStyle::Dots => self.render_dots(false, text_area, target), + DisplayStyle::LastChar => self.render_dots(true, text_area, target), + } } } } diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index 24f81a1e658..7a92458bc1f 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -1,5 +1,5 @@ use crate::{ - strutil::{ShortString, TString}, + strutil::TString, time::Duration, ui::{ component::{ @@ -8,7 +8,7 @@ use crate::{ }, display, event::TouchEvent, - geometry::{Grid, Offset, Rect}, + geometry::{Alignment, Alignment2D, Grid, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg}, keyboard::common::{render_pending_marker, MultiTapKeyboard}, @@ -347,6 +347,8 @@ struct Input { impl Input { const TWITCH: i16 = 4; + const X_STEP: i16 = 12; + const Y_STEP: i16 = 5; fn new() -> Self { Self { @@ -371,28 +373,127 @@ impl Input { self.last_char_timer.stop(); } - fn apply_display_style(&self, passphrase: &ShortString) -> ShortString { - if passphrase.len() == 0 { - ShortString::new() - } else { - match self.display_style { - DisplayStyle::Dots => { - let mut dots = ShortString::new(); - for _ in 0..(passphrase.len()) { - dots.push('*').unwrap(); - } - dots - } - DisplayStyle::Chars => passphrase.clone(), - DisplayStyle::LastChar => { - let mut dots = ShortString::new(); - for _ in 0..(passphrase.len() - 1) { - dots.push('*').unwrap(); - } - // This should not fail because the passphrase is not empty. - dots.push(passphrase.chars().last().unwrap()).unwrap(); - dots + fn render_chars<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { + let style = theme::label_keyboard_mono(); + let mut text_baseline = area.top_left() + Offset::y(style.text_font.text_height()); + let chars = self.textbox.content().len(); + + if chars > 0 { + // Find out how much text can fit into the textbox. + // Accounting for the pending marker, which draws itself one extra pixel + let available_area_width = area.width() - 1; + let truncated = long_line_content_with_ellipsis( + self.textbox.content(), + "", + style.text_font, + available_area_width, + ); + + // Jiggle hidden passphrase when overflowed. + if chars > truncated.len() + && chars % 2 == 0 + && (self.display_style == DisplayStyle::Dots + || self.display_style == DisplayStyle::LastChar) + { + text_baseline.x += Self::TWITCH; + } + + // Paint the visible passphrase. + shape::Text::new(text_baseline, &truncated) + .with_font(style.text_font) + .with_fg(style.text_color) + .render(target); + + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() { + render_pending_marker( + target, + text_baseline, + &truncated, + style.text_font, + style.text_color, + ); + } + } + } + + fn render_dots<'s>(&self, last_char: bool, area: Rect, target: &mut impl Renderer<'s>) { + let style = theme::label_keyboard_mono(); + let dot = theme::ICON_MAGIC.toif; + let mut cursor = area.top_left(); + let all_chars = self.textbox.content().len(); + + if all_chars > 0 { + // Find out how much text can fit into the textbox. + // Accounting for the pending marker, which draws itself one extra pixel + let truncated = long_line_content_with_ellipsis( + self.textbox.content(), + "", + style.text_font, + area.width() - 1, + ); + let visible_chars = truncated.len(); + + // Jiggle when overflowed. + if all_chars > visible_chars + && all_chars % 2 == 0 + && (self.display_style == DisplayStyle::Dots + || self.display_style == DisplayStyle::LastChar) + { + cursor.x += Self::TWITCH; + } + + // Adapt y position for the icons + cursor.y += Self::Y_STEP; + let mut char_idx = 0; + // Small leftmost dot. + if all_chars > visible_chars + 1 { + shape::ToifImage::new(cursor, theme::DOT_SMALL.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(theme::GREY_DARK) + .render(target); + cursor.x += Self::X_STEP; + char_idx += 1; + } + // Greyed out dot. + if all_chars > visible_chars { + shape::ToifImage::new(cursor, theme::DOT_SMALL.toif) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(style.text_color) + .render(target); + cursor.x += Self::X_STEP; + char_idx += 1; + } + // Classical dot(s) + for _ in char_idx..(visible_chars - 1) { + shape::ToifImage::new(cursor, dot) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(style.text_color) + .render(target); + cursor.x += Self::X_STEP; + } + + if last_char { + // Adapt y position for the character + cursor.y = area.top_left().y + style.text_font.text_height(); + // This should not fail because all_chars > 0 + let last = &self.textbox.content()[(all_chars - 1)..all_chars]; + // Paint the last character + shape::Text::new(cursor, last) + .with_align(Alignment::Start) + .with_font(style.text_font) + .with_fg(style.text_color) + .render(target); + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() { + render_pending_marker(target, cursor, last, style.text_font, style.text_color); } + } else { + // Last classical dot + shape::ToifImage::new(cursor, dot) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(style.text_color) + .render(target); } } } @@ -428,47 +529,18 @@ impl Component for Input { } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { - let style = theme::label_keyboard_mono(); - - let mut text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height()) - - Offset::y(style.text_font.text_baseline()); - - let text = self.textbox.content(); - - shape::Bar::new(self.area).with_bg(theme::BG).render(target); + let (text_area, _) = self.area.split_bottom(INPUT_AREA_HEIGHT); - // Find out how much text can fit into the textbox. - // Accounting for the pending marker, which draws itself one pixel longer than - // the last character - let available_area_width = self.area.width() - 1; - let truncated = - long_line_content_with_ellipsis(text, "", style.text_font, available_area_width); + // Paint the background + shape::Bar::new(text_area).with_bg(theme::BG).render(target); - // Jiggle hidden passphrase when overflowed. - if text.len() > truncated.len() - && text.len() % 2 == 0 - && (self.display_style == DisplayStyle::Dots - || self.display_style == DisplayStyle::LastChar) - { - text_baseline.x += Self::TWITCH; - } - - let text_to_display = self.apply_display_style(&truncated); - - shape::Text::new(text_baseline, &text_to_display) - .with_font(style.text_font) - .with_fg(style.text_color) - .render(target); - - // Paint the pending marker. - if self.multi_tap.pending_key().is_some() { - render_pending_marker( - target, - text_baseline, - &text_to_display, - style.text_font, - style.text_color, - ); + // Paint the passphrase + if !self.textbox.content().is_empty() { + match self.display_style { + DisplayStyle::Chars => self.render_chars(text_area, target), + DisplayStyle::Dots => self.render_dots(false, text_area, target), + DisplayStyle::LastChar => self.render_dots(true, text_area, target), + } } } } From b9faca4f339123779c221fcbaa6a38389d50ffb7 Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Thu, 28 Nov 2024 16:29:11 +0100 Subject: [PATCH 05/12] fixup! chore(core): show the last passphrase character for a while --- .../component/keyboard/passphrase.rs | 32 +++++---- .../model_tt/component/keyboard/passphrase.rs | 65 +++++++------------ 2 files changed, 37 insertions(+), 60 deletions(-) diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs index 503fac9f69a..6e0cb875d35 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs @@ -505,11 +505,12 @@ impl Input { } } - fn render_dots<'s>(&self, last_char: bool, area: Rect, target: &mut impl Renderer<'s>) { + fn render_dots<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { let style = theme::label_keyboard_mono(); - let bullet = theme::ICON_PIN_BULLET; + let bullet = theme::ICON_PIN_BULLET.toif; let mut cursor = area.left_center(); let all_chars = self.textbox.content().len(); + let last_char = self.display_style == DisplayStyle::LastChar; if all_chars > 0 { // Find out how much text can fit into the textbox. @@ -521,6 +522,7 @@ impl Input { area.width() - 1, ); let visible_chars = truncated.len(); + let visible_dots = visible_chars - last_char as usize; // Jiggle when overflowed. if all_chars > visible_chars @@ -550,13 +552,16 @@ impl Input { cursor.x += Self::X_STEP; char_idx += 1; } - // Classical dot(s) - for _ in char_idx..(visible_chars - 1) { - shape::ToifImage::new(cursor, bullet) - .with_align(Alignment2D::TOP_LEFT) - .with_fg(style.text_color) - .render(target); - cursor.x += Self::X_STEP; + + if visible_dots > 0 { + // Classical dot(s) + for _ in char_idx..visible_dots { + shape::ToifImage::new(cursor, bullet) + .with_align(Alignment2D::TOP_LEFT) + .with_fg(style.text_color) + .render(target); + cursor.x += Self::X_STEP; + } } if last_char { @@ -574,12 +579,6 @@ impl Input { if self.multi_tap.pending_key().is_some() { render_pending_marker(target, cursor, last, style.text_font, style.text_color); } - } else { - // Last classical dot - shape::ToifImage::new(cursor, bullet) - .with_align(Alignment2D::TOP_LEFT) - .with_fg(style.text_color) - .render(target); } } } @@ -624,8 +623,7 @@ impl Component for Input { if !self.textbox.content().is_empty() { match self.display_style { DisplayStyle::Chars => self.render_chars(text_area, target), - DisplayStyle::Dots => self.render_dots(false, text_area, target), - DisplayStyle::LastChar => self.render_dots(true, text_area, target), + _ => self.render_dots(text_area, target), } } } diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index 7a92458bc1f..3f22b8d31d3 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -1,5 +1,5 @@ use crate::{ - strutil::TString, + strutil::{ShortString, TString}, time::Duration, ui::{ component::{ @@ -8,7 +8,7 @@ use crate::{ }, display, event::TouchEvent, - geometry::{Alignment, Alignment2D, Grid, Offset, Rect}, + geometry::{Alignment, Grid, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg}, keyboard::common::{render_pending_marker, MultiTapKeyboard}, @@ -417,11 +417,11 @@ impl Input { } } - fn render_dots<'s>(&self, last_char: bool, area: Rect, target: &mut impl Renderer<'s>) { + fn render_dots<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { let style = theme::label_keyboard_mono(); - let dot = theme::ICON_MAGIC.toif; - let mut cursor = area.top_left(); + let mut cursor = area.top_left() + Offset::y(style.text_font.text_height()); let all_chars = self.textbox.content().len(); + let last_char = self.display_style == DisplayStyle::LastChar; if all_chars > 0 { // Find out how much text can fit into the textbox. @@ -443,41 +443,27 @@ impl Input { cursor.x += Self::TWITCH; } - // Adapt y position for the icons - cursor.y += Self::Y_STEP; - let mut char_idx = 0; - // Small leftmost dot. - if all_chars > visible_chars + 1 { - shape::ToifImage::new(cursor, theme::DOT_SMALL.toif) - .with_align(Alignment2D::TOP_LEFT) - .with_fg(theme::GREY_DARK) - .render(target); - cursor.x += Self::X_STEP; - char_idx += 1; - } - // Greyed out dot. - if all_chars > visible_chars { - shape::ToifImage::new(cursor, theme::DOT_SMALL.toif) - .with_align(Alignment2D::TOP_LEFT) - .with_fg(style.text_color) - .render(target); - cursor.x += Self::X_STEP; - char_idx += 1; - } - // Classical dot(s) - for _ in char_idx..(visible_chars - 1) { - shape::ToifImage::new(cursor, dot) - .with_align(Alignment2D::TOP_LEFT) - .with_fg(style.text_color) - .render(target); - cursor.x += Self::X_STEP; + let visible_dots = visible_chars - last_char as usize; + let mut dots = ShortString::new(); + for _ in 0..visible_dots { + dots.push('*').unwrap(); } + // Paint the dots + shape::Text::new(cursor, &dots) + .with_align(Alignment::Start) + .with_font(style.text_font) + .with_fg(theme::GREY_MEDIUM) + .render(target); + if last_char { - // Adapt y position for the character - cursor.y = area.top_left().y + style.text_font.text_height(); // This should not fail because all_chars > 0 let last = &self.textbox.content()[(all_chars - 1)..all_chars]; + + // Adapt x position for the character + cursor.x += + style.text_font.text_width(&truncated) - style.text_font.text_width(&last); + // Paint the last character shape::Text::new(cursor, last) .with_align(Alignment::Start) @@ -488,12 +474,6 @@ impl Input { if self.multi_tap.pending_key().is_some() { render_pending_marker(target, cursor, last, style.text_font, style.text_color); } - } else { - // Last classical dot - shape::ToifImage::new(cursor, dot) - .with_align(Alignment2D::TOP_LEFT) - .with_fg(style.text_color) - .render(target); } } } @@ -538,8 +518,7 @@ impl Component for Input { if !self.textbox.content().is_empty() { match self.display_style { DisplayStyle::Chars => self.render_chars(text_area, target), - DisplayStyle::Dots => self.render_dots(false, text_area, target), - DisplayStyle::LastChar => self.render_dots(true, text_area, target), + _ => self.render_dots(text_area, target), } } } From 09c628388f977a8887002e6cfb181b23b31934de Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Fri, 29 Nov 2024 15:19:45 +0100 Subject: [PATCH 06/12] fixup! chore(core): show the last passphrase character for a while --- .../component/keyboard/passphrase.rs | 20 +++++++++------- .../model_tt/component/keyboard/passphrase.rs | 23 ++++++++++++------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs index 6e0cb875d35..a88837c4904 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs @@ -24,7 +24,7 @@ use crate::{ }, }; -use core::{cell::Cell, mem}; +use core::cell::Cell; use num_traits::ToPrimitive; pub enum PassphraseKeyboardMsg { @@ -198,8 +198,14 @@ impl PassphraseKeyboard { Direction::Right => self.active_layout.prev(), _ => self.active_layout, }; - // Clear the pending state. - self.input.multi_tap.clear_pending_state(ctx); + if self.input.multi_tap.pending_key().is_some() { + // Clear the pending state. + self.input.multi_tap.clear_pending_state(ctx); + // the character has been added, show it for a bit and then hide it + self.input + .last_char_timer + .start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S)); + } // Update keys. self.replace_keys_contents(ctx); // Reset backlight to normal level on next paint. @@ -602,11 +608,9 @@ impl Component for Input { self.display_style = DisplayStyle::Chars; ctx.request_paint(); } - Event::Touch(TouchEvent::TouchEnd(_)) => { - if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Chars - { - ctx.request_paint(); - }; + Event::Touch(TouchEvent::TouchEnd(pos)) if self.area.contains(pos) => { + self.display_style = DisplayStyle::Dots; + ctx.request_paint(); } _ => {} } diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index 3f22b8d31d3..d63c3004132 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -19,7 +19,7 @@ use crate::{ util::long_line_content_with_ellipsis, }, }; -use core::{cell::Cell, mem}; +use core::cell::Cell; #[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] pub enum PassphraseKeyboardMsg { @@ -113,8 +113,17 @@ impl PassphraseKeyboard { }; self.scrollbar.go_to(key_page); // Clear the pending state. - self.input - .mutate(ctx, |ctx, i| i.multi_tap.clear_pending_state(ctx)); + + let pending = self + .input + .mutate(ctx, |_ctx, i| i.multi_tap.pending_key().is_some()); + if pending { + self.input + .mutate(ctx, |ctx, i| i.multi_tap.clear_pending_state(ctx)); + // the character has been added, show it for a bit and then hide it + self.input + .mutate(ctx, |ctx, t| t.start_timer(ctx, LAST_DIGIT_TIMEOUT_S)); + } // Update buttons. self.replace_button_content(ctx, key_page); // Reset backlight to normal level on next paint. @@ -497,11 +506,9 @@ impl Component for Input { self.display_style = DisplayStyle::Chars; ctx.request_paint(); } - Event::Touch(TouchEvent::TouchEnd(_)) => { - if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Chars - { - ctx.request_paint(); - }; + Event::Touch(TouchEvent::TouchEnd(pos)) if self.area.contains(pos) => { + self.display_style = DisplayStyle::Dots; + ctx.request_paint(); } _ => {} } From e674efc5018b14089b6d660967c10b4cd03a28fd Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Fri, 29 Nov 2024 18:53:53 +0100 Subject: [PATCH 07/12] fixup! chore(core): show the last passphrase character for a while --- tests/click_tests/common.py | 2 ++ tests/click_tests/test_passphrase_mercury.py | 21 ++++++++++++++++++++ tests/click_tests/test_passphrase_tr.py | 17 ++++++++++++++++ tests/click_tests/test_passphrase_tt.py | 21 ++++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/tests/click_tests/common.py b/tests/click_tests/common.py index b82384a1ca4..d4361f0e84f 100644 --- a/tests/click_tests/common.py +++ b/tests/click_tests/common.py @@ -27,6 +27,8 @@ class CommonPass: EMPTY_ADDRESS = "mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q" + MULTI_CATEGORY = "as12 *&_N" + class PassphraseCategory(Enum): MENU = "MENU" diff --git a/tests/click_tests/test_passphrase_mercury.py b/tests/click_tests/test_passphrase_mercury.py index 9bed04da84a..4a0ad25eb2a 100644 --- a/tests/click_tests/test_passphrase_mercury.py +++ b/tests/click_tests/test_passphrase_mercury.py @@ -70,6 +70,9 @@ assert len(DA_51) == 51 assert DA_51_ADDRESS == DA_50_ADDRESS +# pending + entered character is shown for 1 + 1 seconds, so the delay must be grater +DELAY_S = 2.1 + def get_passphrase_choices(char: str) -> tuple[str, ...]: if char in " *#": @@ -176,6 +179,11 @@ def enter_passphrase(debug: "DebugLink") -> None: debug.click(buttons.MERCURY_YES) +def show_passphrase(debug: "DebugLink") -> None: + """See the passphrase""" + debug.click(buttons.TOP_ROW) + + def delete_char(debug: "DebugLink") -> None: """Deletes the last char""" coords = buttons.pin_passphrase_grid(9) @@ -329,3 +337,16 @@ def test_cycle_through_last_character( passphrase = DA_49 + "i" # for i we need to cycle through "ghi" three times input_passphrase(debug, passphrase) enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler) as debug: + for character in CommonPass.MULTI_CATEGORY: + # insert a digit + input_passphrase(debug, character) + # wait until the last digit is hidden + time.sleep(DELAY_S) + # show the entire PIN + show_passphrase(debug) + enter_passphrase(debug) diff --git a/tests/click_tests/test_passphrase_tr.py b/tests/click_tests/test_passphrase_tr.py index 57685451ba0..79165e93762 100644 --- a/tests/click_tests/test_passphrase_tr.py +++ b/tests/click_tests/test_passphrase_tr.py @@ -14,6 +14,7 @@ # You should have received a copy of the License along with this library. # If not, see . +import time from contextlib import contextmanager from typing import TYPE_CHECKING, Generator, Optional @@ -54,6 +55,9 @@ assert len(AAA_51) == 51 assert AAA_51_ADDRESS == AAA_50_ADDRESS +# entered character is shown for 1 second, so the delay must be grater +DELAY_S = 1.1 + BACK = "inputs__back" SHOW = "inputs__show" @@ -264,3 +268,16 @@ def test_passphrase_loop_all_characters(device_handler: "BackgroundDeviceHandler go_to_category(debug, PassphraseCategory.MENU, use_carousel=False) enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler) as debug: + for character in CommonPass.MULTI_CATEGORY: + # insert a digit + input_passphrase(debug, character) + # wait until the last digit is hidden + time.sleep(DELAY_S) + # show the entire PIN + show_passphrase(debug) + enter_passphrase(debug) diff --git a/tests/click_tests/test_passphrase_tt.py b/tests/click_tests/test_passphrase_tt.py index 8f490c03098..504fac333bf 100644 --- a/tests/click_tests/test_passphrase_tt.py +++ b/tests/click_tests/test_passphrase_tt.py @@ -63,6 +63,9 @@ assert len(DA_51) == 51 assert DA_51_ADDRESS == DA_50_ADDRESS +# pending + entered character is shown for 1 + 1 seconds, so the delay must be grater +DELAY_S = 2.1 + @contextmanager def prepare_passphrase_dialogue( @@ -145,6 +148,11 @@ def enter_passphrase(debug: "DebugLink") -> None: debug.click(coords) +def show_passphrase(debug: "DebugLink") -> None: + """See the passphrase""" + debug.click(buttons.TOP_ROW) + + def delete_char(debug: "DebugLink") -> None: """Deletes the last char""" coords = buttons.pin_passphrase_grid(9) @@ -296,3 +304,16 @@ def test_cycle_through_last_character( passphrase = DA_49 + "i" # for i we need to cycle through "ghi" three times input_passphrase(debug, passphrase) enter_passphrase(debug) + + +@pytest.mark.setup_client(passphrase=True) +def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): + with prepare_passphrase_dialogue(device_handler) as debug: + for character in CommonPass.MULTI_CATEGORY: + # insert a digit + input_passphrase(debug, character) + # wait until the last digit is hidden + time.sleep(DELAY_S) + # show the entire PIN + show_passphrase(debug) + enter_passphrase(debug) From ba52fdf421ce16743174d0f20aafd7f4b1186c3b Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Tue, 3 Dec 2024 07:46:38 +0100 Subject: [PATCH 08/12] fixup! chore(core): show the last passphrase character for a while --- tests/click_tests/test_passphrase_mercury.py | 6 +++--- tests/click_tests/test_passphrase_tr.py | 6 +++--- tests/click_tests/test_passphrase_tt.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/click_tests/test_passphrase_mercury.py b/tests/click_tests/test_passphrase_mercury.py index 4a0ad25eb2a..debd9565eb2 100644 --- a/tests/click_tests/test_passphrase_mercury.py +++ b/tests/click_tests/test_passphrase_mercury.py @@ -343,10 +343,10 @@ def test_cycle_through_last_character( def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): with prepare_passphrase_dialogue(device_handler) as debug: for character in CommonPass.MULTI_CATEGORY: - # insert a digit + # insert a character input_passphrase(debug, character) - # wait until the last digit is hidden + # wait until the last character is hidden time.sleep(DELAY_S) - # show the entire PIN + # show the entire passphrase show_passphrase(debug) enter_passphrase(debug) diff --git a/tests/click_tests/test_passphrase_tr.py b/tests/click_tests/test_passphrase_tr.py index 79165e93762..f6688459002 100644 --- a/tests/click_tests/test_passphrase_tr.py +++ b/tests/click_tests/test_passphrase_tr.py @@ -274,10 +274,10 @@ def test_passphrase_loop_all_characters(device_handler: "BackgroundDeviceHandler def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): with prepare_passphrase_dialogue(device_handler) as debug: for character in CommonPass.MULTI_CATEGORY: - # insert a digit + # insert a character input_passphrase(debug, character) - # wait until the last digit is hidden + # wait until the last character is hidden time.sleep(DELAY_S) - # show the entire PIN + # show the entire passphrase show_passphrase(debug) enter_passphrase(debug) diff --git a/tests/click_tests/test_passphrase_tt.py b/tests/click_tests/test_passphrase_tt.py index 504fac333bf..c3a395354b4 100644 --- a/tests/click_tests/test_passphrase_tt.py +++ b/tests/click_tests/test_passphrase_tt.py @@ -310,10 +310,10 @@ def test_cycle_through_last_character( def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): with prepare_passphrase_dialogue(device_handler) as debug: for character in CommonPass.MULTI_CATEGORY: - # insert a digit + # insert a character input_passphrase(debug, character) - # wait until the last digit is hidden + # wait until the last character is hidden time.sleep(DELAY_S) - # show the entire PIN + # show the entire passphrase show_passphrase(debug) enter_passphrase(debug) From dc65c00243fd62224235819c6f5c05622411a7e7 Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Wed, 4 Dec 2024 22:15:56 +0100 Subject: [PATCH 09/12] fixup! chore(core): show the last passphrase character for a while --- .../model_tt/component/keyboard/passphrase.rs | 177 +++++++----------- 1 file changed, 69 insertions(+), 108 deletions(-) diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index d63c3004132..25d261ec601 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -381,111 +381,6 @@ impl Input { fn stop_timer(&mut self) { self.last_char_timer.stop(); } - - fn render_chars<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { - let style = theme::label_keyboard_mono(); - let mut text_baseline = area.top_left() + Offset::y(style.text_font.text_height()); - let chars = self.textbox.content().len(); - - if chars > 0 { - // Find out how much text can fit into the textbox. - // Accounting for the pending marker, which draws itself one extra pixel - let available_area_width = area.width() - 1; - let truncated = long_line_content_with_ellipsis( - self.textbox.content(), - "", - style.text_font, - available_area_width, - ); - - // Jiggle hidden passphrase when overflowed. - if chars > truncated.len() - && chars % 2 == 0 - && (self.display_style == DisplayStyle::Dots - || self.display_style == DisplayStyle::LastChar) - { - text_baseline.x += Self::TWITCH; - } - - // Paint the visible passphrase. - shape::Text::new(text_baseline, &truncated) - .with_font(style.text_font) - .with_fg(style.text_color) - .render(target); - - // Paint the pending marker. - if self.multi_tap.pending_key().is_some() { - render_pending_marker( - target, - text_baseline, - &truncated, - style.text_font, - style.text_color, - ); - } - } - } - - fn render_dots<'s>(&self, area: Rect, target: &mut impl Renderer<'s>) { - let style = theme::label_keyboard_mono(); - let mut cursor = area.top_left() + Offset::y(style.text_font.text_height()); - let all_chars = self.textbox.content().len(); - let last_char = self.display_style == DisplayStyle::LastChar; - - if all_chars > 0 { - // Find out how much text can fit into the textbox. - // Accounting for the pending marker, which draws itself one extra pixel - let truncated = long_line_content_with_ellipsis( - self.textbox.content(), - "", - style.text_font, - area.width() - 1, - ); - let visible_chars = truncated.len(); - - // Jiggle when overflowed. - if all_chars > visible_chars - && all_chars % 2 == 0 - && (self.display_style == DisplayStyle::Dots - || self.display_style == DisplayStyle::LastChar) - { - cursor.x += Self::TWITCH; - } - - let visible_dots = visible_chars - last_char as usize; - let mut dots = ShortString::new(); - for _ in 0..visible_dots { - dots.push('*').unwrap(); - } - - // Paint the dots - shape::Text::new(cursor, &dots) - .with_align(Alignment::Start) - .with_font(style.text_font) - .with_fg(theme::GREY_MEDIUM) - .render(target); - - if last_char { - // This should not fail because all_chars > 0 - let last = &self.textbox.content()[(all_chars - 1)..all_chars]; - - // Adapt x position for the character - cursor.x += - style.text_font.text_width(&truncated) - style.text_font.text_width(&last); - - // Paint the last character - shape::Text::new(cursor, last) - .with_align(Alignment::Start) - .with_font(style.text_font) - .with_fg(style.text_color) - .render(target); - // Paint the pending marker. - if self.multi_tap.pending_key().is_some() { - render_pending_marker(target, cursor, last, style.text_font, style.text_color); - } - } - } - } } impl Component for Input { @@ -523,9 +418,75 @@ impl Component for Input { // Paint the passphrase if !self.textbox.content().is_empty() { - match self.display_style { - DisplayStyle::Chars => self.render_chars(text_area, target), - _ => self.render_dots(text_area, target), + let style = theme::label_keyboard_mono(); + let mut cursor = text_area.top_left() + Offset::y(style.text_font.text_height()); + let pp_len = self.textbox.content().len(); + + // Find out how much text can fit into the textbox. + // Accounting for the pending marker, which draws itself one extra pixel + let visible = long_line_content_with_ellipsis( + self.textbox.content(), + "", + style.text_font, + text_area.width() - 1, + ); + let pp_visible_len = visible.len(); + + // Jiggle when overflowed. + if pp_len > pp_visible_len + && pp_len % 2 == 0 + && !matches!(self.display_style, DisplayStyle::Chars) + { + cursor.x += Self::TWITCH; + } + + let (visible_dots, visible_chars) = match self.display_style { + DisplayStyle::Dots => (pp_visible_len, 0), + DisplayStyle::Chars => (0, pp_visible_len), + DisplayStyle::LastChar => (pp_visible_len - 1, 1), + }; + + // Render dots if applicable + if visible_dots > 0 { + // First visible_dots stars + let mut dots = ShortString::new(); + for _ in 0..visible_dots { + dots.push('*').unwrap(); + } + + // Paint the grayed-out stars + shape::Text::new(cursor, &dots) + .with_align(Alignment::Start) + .with_font(style.text_font) + .with_fg(theme::GREY_MEDIUM) + .render(target); + + // Adapt x position for the character + cursor.x += style.text_font.text_width(&dots); + } + + // Paint the characters if applicable + if visible_chars > 0 { + // Last visible_chars characters + let start_index = pp_len - visible_chars; + let chars = &self.textbox.content()[start_index..]; + + // Paint the characters + shape::Text::new(cursor, chars) + .with_align(Alignment::Start) + .with_font(style.text_font) + .with_fg(style.text_color) + .render(target); + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() { + render_pending_marker( + target, + cursor, + chars, + style.text_font, + style.text_color, + ); + } } } } From 260468fe83585ce05aa89a2fb4358f0572e5b7bb Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Wed, 4 Dec 2024 22:34:00 +0100 Subject: [PATCH 10/12] fixup! chore(core): show the last passphrase character for a while --- .../component/keyboard/common.rs | 10 ++++++ .../component/keyboard/passphrase.rs | 32 +++++++---------- .../model_mercury/component/keyboard/pin.rs | 26 ++++++-------- .../ui/model_tt/component/keyboard/common.rs | 10 ++++++ .../model_tt/component/keyboard/passphrase.rs | 36 ++++++------------- .../src/ui/model_tt/component/keyboard/pin.rs | 26 ++++++-------- 6 files changed, 63 insertions(+), 77 deletions(-) diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/common.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/common.rs index deea3191949..0182302f3bf 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/common.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/common.rs @@ -168,3 +168,13 @@ pub fn render_pill_shape<'s>( .with_thickness(2) .render(target); } + +/// `DisplayStyle` isused to determine whether the text is fully hidden, fully +/// shown, or partially visible. +#[derive(PartialEq, Debug, Copy, Clone)] +#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] +pub(crate) enum DisplayStyle { + Hidden, + Shown, + LastOnly, +} diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs index a88837c4904..f08d0253dc2 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs @@ -13,7 +13,7 @@ use crate::{ model_mercury::{ component::{ button::{Button, ButtonContent, ButtonMsg}, - keyboard::common::{render_pending_marker, MultiTapKeyboard}, + keyboard::common::{render_pending_marker, DisplayStyle, MultiTapKeyboard}, theme, }, cshape, @@ -32,14 +32,6 @@ pub enum PassphraseKeyboardMsg { Cancelled, } -#[derive(PartialEq, Debug, Copy, Clone)] -#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] -enum DisplayStyle { - Dots, - Chars, - LastChar, -} - /// Enum keeping track of which keyboard is shown and which comes next. Keep the /// number of values and the constant PAGE_COUNT in synch. #[repr(u32)] @@ -415,7 +407,7 @@ impl Component for PassphraseKeyboard { // multi tap timer is runnig, the last digit timer should be stopped self.input.last_char_timer.stop(); } - self.input.display_style = DisplayStyle::LastChar; + self.input.display_style = DisplayStyle::LastOnly; return None; } } @@ -462,7 +454,7 @@ impl Input { area: Rect::zero(), textbox: TextBox::empty(MAX_LENGTH), multi_tap: MultiTapKeyboard::new(), - display_style: DisplayStyle::LastChar, + display_style: DisplayStyle::LastOnly, last_char_timer: Timer::new(), } } @@ -486,8 +478,8 @@ impl Input { // Jiggle hidden passphrase when overflowed. if chars > truncated.len() && chars % 2 == 0 - && (self.display_style == DisplayStyle::Dots - || self.display_style == DisplayStyle::LastChar) + && (self.display_style == DisplayStyle::Hidden + || self.display_style == DisplayStyle::LastOnly) { text_baseline.x += Self::TWITCH; } @@ -516,7 +508,7 @@ impl Input { let bullet = theme::ICON_PIN_BULLET.toif; let mut cursor = area.left_center(); let all_chars = self.textbox.content().len(); - let last_char = self.display_style == DisplayStyle::LastChar; + let last_char = self.display_style == DisplayStyle::LastOnly; if all_chars > 0 { // Find out how much text can fit into the textbox. @@ -533,8 +525,8 @@ impl Input { // Jiggle when overflowed. if all_chars > visible_chars && all_chars % 2 == 0 - && (self.display_style == DisplayStyle::Dots - || self.display_style == DisplayStyle::LastChar) + && (self.display_style == DisplayStyle::Hidden + || self.display_style == DisplayStyle::LastOnly) { cursor.x += Self::TWITCH; } @@ -601,15 +593,15 @@ impl Component for Input { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { match event { Event::Timer(_) if self.last_char_timer.expire(event) => { - self.display_style = DisplayStyle::Dots; + self.display_style = DisplayStyle::Hidden; ctx.request_paint(); } Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => { - self.display_style = DisplayStyle::Chars; + self.display_style = DisplayStyle::Shown; ctx.request_paint(); } Event::Touch(TouchEvent::TouchEnd(pos)) if self.area.contains(pos) => { - self.display_style = DisplayStyle::Dots; + self.display_style = DisplayStyle::Hidden; ctx.request_paint(); } _ => {} @@ -626,7 +618,7 @@ impl Component for Input { // Paint the passphrase if !self.textbox.content().is_empty() { match self.display_style { - DisplayStyle::Chars => self.render_chars(text_area, target), + DisplayStyle::Shown => self.render_chars(text_area, target), _ => self.render_dots(text_area, target), } } 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 59b0e5edeed..9f40a400b77 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 @@ -19,6 +19,7 @@ use crate::{ Button, ButtonContent, ButtonMsg::{self, Clicked}, }, + keyboard::common::DisplayStyle, theme, }, cshape, @@ -431,7 +432,7 @@ impl Component for PinKeyboard<'_> { } // Timeout for showing the last digit. Event::Timer(_) if self.timeout_timer.expire(event) => { - self.textbox.display_style = DisplayStyle::Dots; + self.textbox.display_style = DisplayStyle::Hidden; self.textbox.request_complete_repaint(ctx); ctx.request_paint(); } @@ -482,7 +483,7 @@ impl Component for PinKeyboard<'_> { self.pin_modified(ctx); self.timeout_timer .start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S)); - self.textbox.display_style = DisplayStyle::LastDigit; + self.textbox.display_style = DisplayStyle::LastOnly; self.textbox.request_complete_repaint(ctx); ctx.request_paint(); return None; @@ -542,14 +543,6 @@ struct PinDots { display_style: DisplayStyle, } -#[derive(PartialEq, Debug, Copy, Clone)] -#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] -enum DisplayStyle { - Dots, - Digits, - LastDigit, -} - impl PinDots { const DOT: i16 = 6; const PADDING: i16 = 7; @@ -561,7 +554,7 @@ impl PinDots { pad: Pad::with_background(style.background_color), style, digits: ShortString::new(), - display_style: DisplayStyle::Dots, + display_style: DisplayStyle::Hidden, } } @@ -695,14 +688,15 @@ impl Component for PinDots { match event { Event::Touch(TouchEvent::TouchStart(pos)) => { if self.area.contains(pos) { - self.display_style = DisplayStyle::Digits; + self.display_style = DisplayStyle::Shown; self.pad.clear(); ctx.request_paint(); }; None } Event::Touch(TouchEvent::TouchEnd(_)) => { - if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Digits + if mem::replace(&mut self.display_style, DisplayStyle::Hidden) + == DisplayStyle::Shown { self.pad.clear(); ctx.request_paint(); @@ -717,9 +711,9 @@ impl Component for PinDots { let dot_area = self.area.inset(HEADER_PADDING); self.pad.render(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), + DisplayStyle::Shown => self.render_digits(dot_area, target), + DisplayStyle::Hidden => self.render_dots(false, dot_area, target), + DisplayStyle::LastOnly => self.render_dots(true, dot_area, target), } } } diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs index e59cfb74d1a..6c2ccf8d057 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs @@ -137,3 +137,13 @@ pub fn render_pending_marker<'s>( shape::Bar::new(marker_rect).with_bg(color).render(target); } } + +/// `DisplayStyle` isused to determine whether the text is fully hidden, fully +/// shown, or partially visible. +#[derive(PartialEq, Debug, Copy, Clone)] +#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] +pub(crate) enum DisplayStyle { + Hidden, + Shown, + LastOnly, +} diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index 25d261ec601..f485d4a3b2f 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -11,7 +11,7 @@ use crate::{ geometry::{Alignment, Grid, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg}, - keyboard::common::{render_pending_marker, MultiTapKeyboard}, + keyboard::common::{render_pending_marker, DisplayStyle, MultiTapKeyboard}, swipe::{Swipe, SwipeDirection}, theme, ScrollBar, }, @@ -27,14 +27,6 @@ pub enum PassphraseKeyboardMsg { Cancelled, } -#[derive(PartialEq, Debug, Copy, Clone)] -#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] -enum DisplayStyle { - Dots, - Chars, - LastChar, -} - pub struct PassphraseKeyboard { page_swipe: Swipe, input: Child, @@ -324,7 +316,7 @@ impl Component for PassphraseKeyboard { self.input.mutate(ctx, |_ctx, t| t.stop_timer()); } self.input - .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::LastChar)); + .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::LastOnly)); return None; } } @@ -364,7 +356,7 @@ impl Input { area: Rect::zero(), textbox: TextBox::empty(MAX_LENGTH), multi_tap: MultiTapKeyboard::new(), - display_style: DisplayStyle::LastChar, + display_style: DisplayStyle::LastOnly, last_char_timer: Timer::new(), } } @@ -394,15 +386,15 @@ impl Component for Input { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { match event { Event::Timer(_) if self.last_char_timer.expire(event) => { - self.display_style = DisplayStyle::Dots; + self.display_style = DisplayStyle::Hidden; ctx.request_paint(); } Event::Touch(TouchEvent::TouchStart(pos)) if self.area.contains(pos) => { - self.display_style = DisplayStyle::Chars; + self.display_style = DisplayStyle::Shown; ctx.request_paint(); } Event::Touch(TouchEvent::TouchEnd(pos)) if self.area.contains(pos) => { - self.display_style = DisplayStyle::Dots; + self.display_style = DisplayStyle::Hidden; ctx.request_paint(); } _ => {} @@ -435,15 +427,15 @@ impl Component for Input { // Jiggle when overflowed. if pp_len > pp_visible_len && pp_len % 2 == 0 - && !matches!(self.display_style, DisplayStyle::Chars) + && !matches!(self.display_style, DisplayStyle::Shown) { cursor.x += Self::TWITCH; } let (visible_dots, visible_chars) = match self.display_style { - DisplayStyle::Dots => (pp_visible_len, 0), - DisplayStyle::Chars => (0, pp_visible_len), - DisplayStyle::LastChar => (pp_visible_len - 1, 1), + DisplayStyle::Hidden => (pp_visible_len, 0), + DisplayStyle::Shown => (0, pp_visible_len), + DisplayStyle::LastOnly => (pp_visible_len - 1, 1), }; // Render dots if applicable @@ -479,13 +471,7 @@ impl Component for Input { .render(target); // Paint the pending marker. if self.multi_tap.pending_key().is_some() { - render_pending_marker( - target, - cursor, - chars, - style.text_font, - style.text_color, - ); + render_pending_marker(target, cursor, chars, style.text_font, style.text_color); } } } 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 a430e279d36..5f2936ea469 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 @@ -17,6 +17,7 @@ use crate::{ Button, ButtonContent, ButtonMsg::{self, Clicked}, }, + keyboard::common::DisplayStyle, theme, }, shape::{self, Renderer}, @@ -218,7 +219,7 @@ impl Component for PinKeyboard<'_> { // Timeout for showing the last digit. Event::Timer(_) if self.timeout_timer.expire(event) => { self.textbox - .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::Dots)); + .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::Hidden)); self.textbox.request_complete_repaint(ctx); ctx.request_paint(); } @@ -255,7 +256,7 @@ impl Component for PinKeyboard<'_> { self.timeout_timer .start(ctx, Duration::from_secs(LAST_DIGIT_TIMEOUT_S)); self.textbox - .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::LastDigit)); + .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::LastOnly)); self.textbox.request_complete_repaint(ctx); ctx.request_paint(); return None; @@ -294,14 +295,6 @@ struct PinDots { display_style: DisplayStyle, } -#[derive(PartialEq, Debug, Copy, Clone)] -#[cfg_attr(feature = "ui_debug", derive(ufmt::derive::uDebug))] -enum DisplayStyle { - Dots, - Digits, - LastDigit, -} - impl PinDots { const DOT: i16 = 6; const PADDING: i16 = 6; @@ -315,7 +308,7 @@ impl PinDots { pad: Pad::with_background(style.background_color), style, digits, - display_style: DisplayStyle::Dots, + display_style: DisplayStyle::Hidden, } } @@ -450,14 +443,15 @@ impl Component for PinDots { match event { Event::Touch(TouchEvent::TouchStart(pos)) => { if self.area.contains(pos) { - self.display_style = DisplayStyle::Digits; + self.display_style = DisplayStyle::Shown; self.pad.clear(); ctx.request_paint(); }; None } Event::Touch(TouchEvent::TouchEnd(_)) => { - if mem::replace(&mut self.display_style, DisplayStyle::Dots) == DisplayStyle::Digits + if mem::replace(&mut self.display_style, DisplayStyle::Hidden) + == DisplayStyle::Shown { self.pad.clear(); ctx.request_paint(); @@ -473,9 +467,9 @@ impl Component for PinDots { self.pad.render(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), + DisplayStyle::Shown => self.render_digits(dot_area, target), + DisplayStyle::Hidden => self.render_dots(false, dot_area, target), + DisplayStyle::LastOnly => self.render_dots(true, dot_area, target), } } } From e8f685253b1aa887ce7dec0f869347ef2f67d6a7 Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Wed, 4 Dec 2024 23:03:27 +0100 Subject: [PATCH 11/12] fixup! chore(core): show the last passphrase character for a while --- .../src/ui/model_mercury/component/keyboard/passphrase.rs | 2 ++ .../rust/src/ui/model_tt/component/keyboard/passphrase.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs index f08d0253dc2..da5a7cec75b 100644 --- a/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_mercury/component/keyboard/passphrase.rs @@ -369,12 +369,14 @@ impl Component for PassphraseKeyboard { self.input.multi_tap.clear_pending_state(ctx); self.input.textbox.delete_last(ctx); self.after_edit(ctx); + self.input.display_style = DisplayStyle::Hidden; return None; } Some(ButtonMsg::LongPressed) => { self.input.multi_tap.clear_pending_state(ctx); self.input.textbox.clear(ctx); self.after_edit(ctx); + self.input.display_style = DisplayStyle::Hidden; return None; } _ => {} diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index f485d4a3b2f..48b29bda832 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -273,6 +273,8 @@ impl Component for PassphraseKeyboard { i.textbox.delete_last(ctx); }); self.after_edit(ctx); + self.input + .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::Hidden)); None }; } @@ -282,6 +284,8 @@ impl Component for PassphraseKeyboard { i.textbox.clear(ctx); }); self.after_edit(ctx); + self.input + .mutate(ctx, |_ctx, t| t.set_display_style(DisplayStyle::Hidden)); return None; } _ => {} From 7c3615e60b435da4a9fd397b946c32139788948a Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Mon, 16 Dec 2024 14:53:29 +0100 Subject: [PATCH 12/12] fixup! chore(core): show the last passphrase character for a while --- tests/click_tests/test_passphrase_mercury.py | 4 ++-- tests/click_tests/test_passphrase_tr.py | 4 ++-- tests/click_tests/test_passphrase_tt.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/click_tests/test_passphrase_mercury.py b/tests/click_tests/test_passphrase_mercury.py index debd9565eb2..9cdfd930908 100644 --- a/tests/click_tests/test_passphrase_mercury.py +++ b/tests/click_tests/test_passphrase_mercury.py @@ -347,6 +347,6 @@ def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): input_passphrase(debug, character) # wait until the last character is hidden time.sleep(DELAY_S) - # show the entire passphrase - show_passphrase(debug) + # show the entire passphrase + show_passphrase(debug) enter_passphrase(debug) diff --git a/tests/click_tests/test_passphrase_tr.py b/tests/click_tests/test_passphrase_tr.py index f6688459002..79c979fe1f9 100644 --- a/tests/click_tests/test_passphrase_tr.py +++ b/tests/click_tests/test_passphrase_tr.py @@ -278,6 +278,6 @@ def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): input_passphrase(debug, character) # wait until the last character is hidden time.sleep(DELAY_S) - # show the entire passphrase - show_passphrase(debug) + # show the entire passphrase + show_passphrase(debug) enter_passphrase(debug) diff --git a/tests/click_tests/test_passphrase_tt.py b/tests/click_tests/test_passphrase_tt.py index c3a395354b4..6a3fa875890 100644 --- a/tests/click_tests/test_passphrase_tt.py +++ b/tests/click_tests/test_passphrase_tt.py @@ -314,6 +314,6 @@ def test_last_char_timeout(device_handler: "BackgroundDeviceHandler"): input_passphrase(debug, character) # wait until the last character is hidden time.sleep(DELAY_S) - # show the entire passphrase - show_passphrase(debug) + # show the entire passphrase + show_passphrase(debug) enter_passphrase(debug)