1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-18 02:22:01 +00:00

chore(core): show the last passphrase character for a while

This commit is contained in:
Lukas Bielesch 2024-11-18 18:28:54 +01:00
parent 997c27adb3
commit 02b2bf99e3
6 changed files with 244 additions and 31 deletions

View File

@ -0,0 +1 @@
Show last typed passphrase character for short period of time

View File

@ -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<Self::Msg> {
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<Self::Msg> {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
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)

View File

@ -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)
}

View File

@ -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<Self::Msg> {
// 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 {
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);
}
}
// 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 self.show_last_digit {
self.show_last_digit = 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();

View File

@ -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<Input>,
@ -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<Self::Msg> {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
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)

View File

@ -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)
}