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

fix(core/rust): optimize keyboard behavior

[no changelog]
This commit is contained in:
tychovrahe 2022-11-30 15:40:55 +01:00 committed by TychoVrahe
parent fc3ee87c25
commit 387af03842
8 changed files with 93 additions and 40 deletions

View File

@ -38,6 +38,14 @@ pub fn complete_word(prefix: &str) -> Option<&'static str> {
} }
} }
pub fn options_num(prefix: &str) -> Option<usize> {
if prefix.is_empty() {
None
} else {
Some(Wordlist::all().filter_prefix(prefix).len())
}
}
pub fn word_completion_mask(prefix: &str) -> u32 { pub fn word_completion_mask(prefix: &str) -> u32 {
// SAFETY: `mnemonic_word_completion_mask` shouldn't retain nor modify the // SAFETY: `mnemonic_word_completion_mask` shouldn't retain nor modify the
// passed byte string, making the call safe. // passed byte string, making the call safe.

View File

@ -247,11 +247,6 @@ where
} }
Event::Touch(TouchEvent::TouchMove(pos)) => { Event::Touch(TouchEvent::TouchMove(pos)) => {
match self.state { match self.state {
State::Released if self.area.contains(pos) => {
// Touch entered our area, transform to `Pressed` state.
self.set(ctx, State::Pressed);
return Some(ButtonMsg::Pressed);
}
State::Pressed if !self.area.contains(pos) => { State::Pressed if !self.area.contains(pos) => {
// Touch is leaving our area, transform to `Released` state. // Touch is leaving our area, transform to `Released` state.
self.set(ctx, State::Released); self.set(ctx, State::Released);

View File

@ -23,6 +23,7 @@ pub struct Bip39Input {
button: Button<&'static str>, button: Button<&'static str>,
textbox: TextBox<MAX_LENGTH>, textbox: TextBox<MAX_LENGTH>,
multi_tap: MultiTapKeyboard, multi_tap: MultiTapKeyboard,
options_num: Option<usize>,
suggested_word: Option<&'static str>, suggested_word: Option<&'static str>,
} }
@ -61,6 +62,14 @@ impl MnemonicInput for Bip39Input {
self.complete_word_from_dictionary(ctx); self.complete_word_from_dictionary(ctx);
} }
/// Backspace button was long pressed, let's delete all characters of input
/// and clear the pending marker.
fn on_backspace_long_press(&mut self, ctx: &mut EventCtx) {
self.multi_tap.clear_pending_state(ctx);
self.textbox.clear(ctx);
self.complete_word_from_dictionary(ctx);
}
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
self.textbox.is_empty() self.textbox.is_empty()
} }
@ -145,6 +154,7 @@ impl Bip39Input {
button: Button::empty(), button: Button::empty(),
textbox: TextBox::empty(), textbox: TextBox::empty(),
multi_tap: MultiTapKeyboard::new(), multi_tap: MultiTapKeyboard::new(),
options_num: None,
suggested_word: None, suggested_word: None,
} }
} }
@ -164,17 +174,19 @@ impl Bip39Input {
/// Input button was clicked. If the content matches the suggested word, /// Input button was clicked. If the content matches the suggested word,
/// let's confirm it, otherwise just auto-complete. /// let's confirm it, otherwise just auto-complete.
fn on_input_click(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg> { fn on_input_click(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg> {
match self.suggested_word { if let (Some(word), Some(num)) = (self.suggested_word, self.options_num) {
Some(word) if word == self.textbox.content() => { return if num == 1 && word.starts_with(self.textbox.content()) {
// Confirm button.
self.textbox.clear(ctx); self.textbox.clear(ctx);
Some(MnemonicInputMsg::Confirmed) Some(MnemonicInputMsg::Confirmed)
} } else {
Some(word) => { // Auto-complete button.
self.textbox.replace(ctx, word); self.textbox.replace(ctx, word);
self.complete_word_from_dictionary(ctx);
Some(MnemonicInputMsg::Completed) Some(MnemonicInputMsg::Completed)
};
} }
None => None, None
}
} }
/// Timeout occurred. If we can auto-complete current input, let's just /// Timeout occurred. If we can auto-complete current input, let's just
@ -190,11 +202,12 @@ impl Bip39Input {
} }
fn complete_word_from_dictionary(&mut self, ctx: &mut EventCtx) { fn complete_word_from_dictionary(&mut self, ctx: &mut EventCtx) {
self.options_num = bip39::options_num(self.textbox.content());
self.suggested_word = bip39::complete_word(self.textbox.content()); self.suggested_word = bip39::complete_word(self.textbox.content());
// Change the style of the button depending on the completed word. // Change the style of the button depending on the completed word.
if let Some(word) = self.suggested_word { if let (Some(word), Some(num)) = (self.suggested_word, self.options_num) {
if self.textbox.content() == word { if num == 1 && word.starts_with(self.textbox.content()) {
// Confirm button. // Confirm button.
self.button.enable(ctx); self.button.enable(ctx);
self.button.set_stylesheet(ctx, theme::button_confirm()); self.button.set_stylesheet(ctx, theme::button_confirm());

View File

@ -44,7 +44,8 @@ where
theme::ICON_BACK, theme::ICON_BACK,
Offset::new(30, 17), Offset::new(30, 17),
) )
.styled(theme::button_clear()), .styled(theme::button_clear())
.with_long_press(theme::ERASE_HOLD_DURATION),
)), )),
input: Child::new(Maybe::hidden(theme::BG, input)), input: Child::new(Maybe::hidden(theme::BG, input)),
keys: T::keys().map(Button::with_text).map(Child::new), keys: T::keys().map(Button::with_text).map(Child::new),
@ -126,12 +127,22 @@ where
} }
_ => {} _ => {}
} }
if let Some(ButtonMsg::Clicked) = self.back.event(ctx, event) {
match self.back.event(ctx, event) {
Some(ButtonMsg::Clicked) => {
self.input self.input
.mutate(ctx, |ctx, i| i.inner_mut().on_backspace_click(ctx)); .mutate(ctx, |ctx, i| i.inner_mut().on_backspace_click(ctx));
self.on_input_change(ctx); self.on_input_change(ctx);
return None; return None;
} }
Some(ButtonMsg::LongPressed) => {
self.input
.mutate(ctx, |ctx, i| i.inner_mut().on_backspace_long_press(ctx));
self.on_input_change(ctx);
return None;
}
_ => {}
}
for (key, btn) in self.keys.iter_mut().enumerate() { for (key, btn) in self.keys.iter_mut().enumerate() {
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) { if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
self.input self.input
@ -165,6 +176,7 @@ pub trait MnemonicInput: Component<Msg = MnemonicInputMsg> {
fn can_key_press_lead_to_a_valid_word(&self, key: usize) -> bool; fn can_key_press_lead_to_a_valid_word(&self, key: usize) -> bool;
fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize); fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize);
fn on_backspace_click(&mut self, ctx: &mut EventCtx); fn on_backspace_click(&mut self, ctx: &mut EventCtx);
fn on_backspace_long_press(&mut self, ctx: &mut EventCtx);
fn is_empty(&self) -> bool; fn is_empty(&self) -> bool;
fn mnemonic(&self) -> Option<&'static str>; fn mnemonic(&self) -> Option<&'static str>;
} }

View File

@ -3,7 +3,7 @@ use crate::ui::{
display, display,
geometry::{Grid, Insets, Offset, Rect}, geometry::{Grid, Insets, Offset, Rect},
model_tt::component::{ model_tt::component::{
button::{Button, ButtonContent, ButtonMsg::Clicked}, button::{Button, ButtonContent, ButtonMsg},
keyboard::common::{ keyboard::common::{
paint_pending_marker, MultiTapKeyboard, TextBox, HEADER_HEIGHT, HEADER_PADDING_BOTTOM, paint_pending_marker, MultiTapKeyboard, TextBox, HEADER_HEIGHT, HEADER_PADDING_BOTTOM,
HEADER_PADDING_SIDE, HEADER_PADDING_SIDE,
@ -56,6 +56,7 @@ impl PassphraseKeyboard {
) )
.styled(theme::button_reset()) .styled(theme::button_reset())
.initially_enabled(false) .initially_enabled(false)
.with_long_press(theme::ERASE_HOLD_DURATION)
.into_child(), .into_child(),
keys: KEYBOARD.map(|page| { keys: KEYBOARD.map(|page| {
page.map(|text| { page.map(|text| {
@ -192,11 +193,13 @@ impl Component for PassphraseKeyboard {
self.on_page_swipe(ctx, swipe); self.on_page_swipe(ctx, swipe);
return None; return None;
} }
if let Some(Clicked) = self.confirm.event(ctx, event) { if let Some(ButtonMsg::Clicked) = self.confirm.event(ctx, event) {
// Confirm button was clicked, we're done. // Confirm button was clicked, we're done.
return Some(PassphraseKeyboardMsg::Confirmed); return Some(PassphraseKeyboardMsg::Confirmed);
} }
if let Some(Clicked) = self.back.event(ctx, event) {
match self.back.event(ctx, event) {
Some(ButtonMsg::Clicked) => {
// Backspace button was clicked. If we have any content in the textbox, let's // Backspace button was clicked. If we have any content in the textbox, let's
// delete the last character. Otherwise cancel. // delete the last character. Otherwise cancel.
return if self.input.inner().textbox.is_empty() { return if self.input.inner().textbox.is_empty() {
@ -210,13 +213,23 @@ impl Component for PassphraseKeyboard {
None None
}; };
} }
Some(ButtonMsg::LongPressed) => {
self.input.mutate(ctx, |ctx, i| {
i.multi_tap.clear_pending_state(ctx);
i.textbox.clear(ctx);
});
self.after_edit(ctx);
return None;
}
_ => {}
}
// Process key button events in case we did not reach maximum passphrase length. // Process key button events in case we did not reach maximum passphrase length.
// (All input buttons should be disallowed in that case, this is just a safety // (All input buttons should be disallowed in that case, this is just a safety
// measure.) // measure.)
if !self.input.inner().textbox.is_full() { if !self.input.inner().textbox.is_full() {
for (key, btn) in self.keys[self.scrollbar.active_page].iter_mut().enumerate() { for (key, btn) in self.keys[self.scrollbar.active_page].iter_mut().enumerate() {
if let Some(Clicked) = btn.event(ctx, event) { if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
// Key button was clicked. If this button is pending, let's cycle the pending // Key button was clicked. If this button is pending, let's cycle the pending
// character in textbox. If not, let's just append the first character. // character in textbox. If not, let's just append the first character.
let text = Self::key_text(btn.inner().content()); let text = Self::key_text(btn.inner().content());
@ -287,7 +300,7 @@ impl Component for Input {
} }
fn paint(&mut self) { fn paint(&mut self) {
let style = theme::label_default(); let style = theme::label_keyboard();
let mut 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()); - Offset::y(style.text_font.text_baseline());

View File

@ -28,7 +28,6 @@ const MAX_LENGTH: usize = 50;
const MAX_VISIBLE_DOTS: usize = 14; const MAX_VISIBLE_DOTS: usize = 14;
const MAX_VISIBLE_DIGITS: usize = 16; const MAX_VISIBLE_DIGITS: usize = 16;
const DIGIT_COUNT: usize = 10; // 0..10 const DIGIT_COUNT: usize = 10; // 0..10
const ERASE_HOLD_DURATION: Duration = Duration::from_secs(2);
const HEADER_HEIGHT: i16 = 25; const HEADER_HEIGHT: i16 = 25;
const HEADER_PADDING_SIDE: i16 = 5; const HEADER_PADDING_SIDE: i16 = 5;
@ -76,7 +75,7 @@ where
Offset::new(30, 12), Offset::new(30, 12),
) )
.styled(theme::button_reset()) .styled(theme::button_reset())
.with_long_press(ERASE_HOLD_DURATION) .with_long_press(theme::ERASE_HOLD_DURATION)
.initially_enabled(false); .initially_enabled(false);
let erase_btn = Maybe::hidden(theme::BG, erase_btn).into_child(); let erase_btn = Maybe::hidden(theme::BG, erase_btn).into_child();

View File

@ -78,6 +78,14 @@ impl MnemonicInput for Slip39Input {
self.complete_word_from_dictionary(ctx); self.complete_word_from_dictionary(ctx);
} }
/// Backspace button was long pressed, let's delete all characters of input
/// and clear the pending marker.
fn on_backspace_long_press(&mut self, ctx: &mut EventCtx) {
self.multi_tap.clear_pending_state(ctx);
self.textbox.clear(ctx);
self.complete_word_from_dictionary(ctx);
}
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
self.textbox.is_empty() self.textbox.is_empty()
} }

View File

@ -1,16 +1,21 @@
use crate::ui::{ use crate::{
time::Duration,
ui::{
component::{ component::{
text::{formatted::FormattedFonts, LineBreaking, PageBreaking, TextStyle}, text::{formatted::FormattedFonts, LineBreaking, PageBreaking, TextStyle},
FixedHeightBar, FixedHeightBar,
}, },
display::{Color, Font}, display::{Color, Font},
geometry::Insets, geometry::Insets,
},
}; };
use super::component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet}; use super::component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet};
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
pub const ERASE_HOLD_DURATION: Duration = Duration::from_millis(1500);
// Typical backlight values. // Typical backlight values.
pub const BACKLIGHT_NORMAL: i32 = 150; pub const BACKLIGHT_NORMAL: i32 = 150;
pub const BACKLIGHT_LOW: i32 = 45; pub const BACKLIGHT_LOW: i32 = 45;