1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-04 21:48:17 +00:00
This commit is contained in:
Lukáš Bielesch 2024-12-03 12:08:32 +02:00 committed by GitHub
commit 5264a3de59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 485 additions and 82 deletions

View File

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

View File

@ -1,13 +1,15 @@
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,
geometry::{Alignment, Direction, Grid, Insets, Offset, Rect},
event::TouchEvent,
geometry::{Alignment, Alignment2D, Direction, Grid, Insets, Offset, Rect},
model_mercury::{
component::{
button::{Button, ButtonContent, ButtonMsg},
@ -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 {
@ -186,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.
@ -272,7 +290,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 +301,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);
@ -323,8 +339,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 +405,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 +449,143 @@ struct Input {
area: Rect,
textbox: TextBox,
multi_tap: MultiTapKeyboard,
display_style: DisplayStyle,
last_char_timer: Timer,
}
impl Input {
const TWITCH: i16 = 4;
const X_STEP: i16 = 13;
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 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 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.
// 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();
let visible_dots = visible_chars - last_char as usize;
// 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;
}
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 {
// 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);
}
}
}
}
}
@ -436,41 +598,37 @@ 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(pos)) if self.area.contains(pos) => {
self.display_style = DisplayStyle::Dots;
ctx.request_paint();
}
_ => {}
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = theme::label_keyboard();
let text_area = self.area.inset(INPUT_INSETS);
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
- Offset::y(style.text_font.text_baseline());
// Paint the background
shape::Bar::new(text_area).with_bg(theme::BG).render(target);
let text = self.textbox.content();
shape::Bar::new(self.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);
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),
_ => self.render_dots(text_area, target),
}
}
}
}

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 {
@ -281,11 +287,14 @@ 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),
current_category: ChoiceCategory::Menu,
last_char_timer: Timer::new(),
}
}
@ -383,17 +392,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 +451,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,23 +1,24 @@
use crate::{
strutil::TString,
strutil::{ShortString, TString},
time::Duration,
ui::{
component::{
base::ComponentExt, text::common::TextBox, Child, Component, Event, EventCtx, Never,
Timer,
},
display,
geometry::{Grid, Offset, Rect},
event::TouchEvent,
geometry::{Alignment, Grid, Offset, Rect},
model_tt::component::{
button::{Button, ButtonContent, ButtonMsg},
keyboard::common::{render_pending_marker, MultiTapKeyboard},
swipe::{Swipe, SwipeDirection},
theme, ScrollBar,
},
shape,
shape::Renderer,
shape::{self, Renderer},
util::long_line_content_with_ellipsis,
},
};
use core::cell::Cell;
#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))]
@ -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 {
@ -102,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.
@ -189,7 +209,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 +251,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 +314,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 +350,140 @@ struct Input {
area: Rect,
textbox: TextBox,
multi_tap: MultiTapKeyboard,
display_style: DisplayStyle,
last_char_timer: Timer,
}
impl Input {
const TWITCH: i16 = 4;
const X_STEP: i16 = 12;
const Y_STEP: i16 = 5;
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 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);
}
}
}
}
}
@ -333,41 +496,37 @@ 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(pos)) if self.area.contains(pos) => {
self.display_style = DisplayStyle::Dots;
ctx.request_paint();
}
_ => {}
}
None
}
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
let style = theme::label_keyboard();
let (text_area, _) = self.area.split_bottom(INPUT_AREA_HEIGHT);
let text_baseline = self.area.top_left() + Offset::y(style.text_font.text_height())
- Offset::y(style.text_font.text_baseline());
// Paint the background
shape::Bar::new(text_area).with_bg(theme::BG).render(target);
let text = self.textbox.content();
shape::Bar::new(self.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);
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),
_ => self.render_dots(text_area, target),
}
}
}
}

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

View File

@ -27,6 +27,8 @@ class CommonPass:
EMPTY_ADDRESS = "mvbu1Gdy8SUjTenqerxUaZyYjmveZvt33q"
MULTI_CATEGORY = "as12 *&_N"
class PassphraseCategory(Enum):
MENU = "MENU"

View File

@ -70,6 +70,9 @@ DA_51_ADDRESS = DA_50_ADDRESS
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 character
input_passphrase(debug, character)
# wait until the last character is hidden
time.sleep(DELAY_S)
# show the entire passphrase
show_passphrase(debug)
enter_passphrase(debug)

View File

@ -14,6 +14,7 @@
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import time
from contextlib import contextmanager
from typing import TYPE_CHECKING, Generator, Optional
@ -54,6 +55,9 @@ AAA_51_ADDRESS = "miPeCUxf1Ufh5DtV3AuBopNM8YEDvnQZMh"
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 character
input_passphrase(debug, character)
# wait until the last character is hidden
time.sleep(DELAY_S)
# show the entire passphrase
show_passphrase(debug)
enter_passphrase(debug)

View File

@ -63,6 +63,9 @@ DA_51_ADDRESS = DA_50_ADDRESS
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 character
input_passphrase(debug, character)
# wait until the last character is hidden
time.sleep(DELAY_S)
# show the entire passphrase
show_passphrase(debug)
enter_passphrase(debug)