1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-26 01:18:28 +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 {
// SAFETY: `mnemonic_word_completion_mask` shouldn't retain nor modify the
// passed byte string, making the call safe.

View File

@ -247,11 +247,6 @@ where
}
Event::Touch(TouchEvent::TouchMove(pos)) => {
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) => {
// Touch is leaving our area, transform to `Released` state.
self.set(ctx, State::Released);

View File

@ -23,6 +23,7 @@ pub struct Bip39Input {
button: Button<&'static str>,
textbox: TextBox<MAX_LENGTH>,
multi_tap: MultiTapKeyboard,
options_num: Option<usize>,
suggested_word: Option<&'static str>,
}
@ -61,6 +62,14 @@ impl MnemonicInput for Bip39Input {
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 {
self.textbox.is_empty()
}
@ -145,6 +154,7 @@ impl Bip39Input {
button: Button::empty(),
textbox: TextBox::empty(),
multi_tap: MultiTapKeyboard::new(),
options_num: None,
suggested_word: None,
}
}
@ -164,17 +174,19 @@ impl Bip39Input {
/// Input button was clicked. If the content matches the suggested word,
/// let's confirm it, otherwise just auto-complete.
fn on_input_click(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg> {
match self.suggested_word {
Some(word) if word == self.textbox.content() => {
if let (Some(word), Some(num)) = (self.suggested_word, self.options_num) {
return if num == 1 && word.starts_with(self.textbox.content()) {
// Confirm button.
self.textbox.clear(ctx);
Some(MnemonicInputMsg::Confirmed)
}
Some(word) => {
} else {
// Auto-complete button.
self.textbox.replace(ctx, word);
self.complete_word_from_dictionary(ctx);
Some(MnemonicInputMsg::Completed)
}
None => None,
};
}
None
}
/// 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) {
self.options_num = bip39::options_num(self.textbox.content());
self.suggested_word = bip39::complete_word(self.textbox.content());
// Change the style of the button depending on the completed word.
if let Some(word) = self.suggested_word {
if self.textbox.content() == word {
if let (Some(word), Some(num)) = (self.suggested_word, self.options_num) {
if num == 1 && word.starts_with(self.textbox.content()) {
// Confirm button.
self.button.enable(ctx);
self.button.set_stylesheet(ctx, theme::button_confirm());

View File

@ -44,7 +44,8 @@ where
theme::ICON_BACK,
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)),
keys: T::keys().map(Button::with_text).map(Child::new),
@ -126,11 +127,21 @@ where
}
_ => {}
}
if let Some(ButtonMsg::Clicked) = self.back.event(ctx, event) {
self.input
.mutate(ctx, |ctx, i| i.inner_mut().on_backspace_click(ctx));
self.on_input_change(ctx);
return None;
match self.back.event(ctx, event) {
Some(ButtonMsg::Clicked) => {
self.input
.mutate(ctx, |ctx, i| i.inner_mut().on_backspace_click(ctx));
self.on_input_change(ctx);
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() {
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
@ -165,6 +176,7 @@ pub trait MnemonicInput: Component<Msg = MnemonicInputMsg> {
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_backspace_click(&mut self, ctx: &mut EventCtx);
fn on_backspace_long_press(&mut self, ctx: &mut EventCtx);
fn is_empty(&self) -> bool;
fn mnemonic(&self) -> Option<&'static str>;
}

View File

@ -3,7 +3,7 @@ use crate::ui::{
display,
geometry::{Grid, Insets, Offset, Rect},
model_tt::component::{
button::{Button, ButtonContent, ButtonMsg::Clicked},
button::{Button, ButtonContent, ButtonMsg},
keyboard::common::{
paint_pending_marker, MultiTapKeyboard, TextBox, HEADER_HEIGHT, HEADER_PADDING_BOTTOM,
HEADER_PADDING_SIDE,
@ -56,6 +56,7 @@ impl PassphraseKeyboard {
)
.styled(theme::button_reset())
.initially_enabled(false)
.with_long_press(theme::ERASE_HOLD_DURATION)
.into_child(),
keys: KEYBOARD.map(|page| {
page.map(|text| {
@ -192,23 +193,35 @@ impl Component for PassphraseKeyboard {
self.on_page_swipe(ctx, swipe);
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.
return Some(PassphraseKeyboardMsg::Confirmed);
}
if let Some(Clicked) = self.back.event(ctx, event) {
// Backspace button was clicked. If we have any content in the textbox, let's
// delete the last character. Otherwise cancel.
return if self.input.inner().textbox.is_empty() {
Some(PassphraseKeyboardMsg::Cancelled)
} else {
match self.back.event(ctx, event) {
Some(ButtonMsg::Clicked) => {
// Backspace button was clicked. If we have any content in the textbox, let's
// delete the last character. Otherwise cancel.
return if self.input.inner().textbox.is_empty() {
Some(PassphraseKeyboardMsg::Cancelled)
} else {
self.input.mutate(ctx, |ctx, i| {
i.multi_tap.clear_pending_state(ctx);
i.textbox.delete_last(ctx);
});
self.after_edit(ctx);
None
};
}
Some(ButtonMsg::LongPressed) => {
self.input.mutate(ctx, |ctx, i| {
i.multi_tap.clear_pending_state(ctx);
i.textbox.delete_last(ctx);
i.textbox.clear(ctx);
});
self.after_edit(ctx);
None
};
return None;
}
_ => {}
}
// Process key button events in case we did not reach maximum passphrase length.
@ -216,7 +229,7 @@ impl Component for PassphraseKeyboard {
// measure.)
if !self.input.inner().textbox.is_full() {
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
// character in textbox. If not, let's just append the first character.
let text = Self::key_text(btn.inner().content());
@ -287,7 +300,7 @@ impl Component for Input {
}
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())
- 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_DIGITS: usize = 16;
const DIGIT_COUNT: usize = 10; // 0..10
const ERASE_HOLD_DURATION: Duration = Duration::from_secs(2);
const HEADER_HEIGHT: i16 = 25;
const HEADER_PADDING_SIDE: i16 = 5;
@ -76,7 +75,7 @@ where
Offset::new(30, 12),
)
.styled(theme::button_reset())
.with_long_press(ERASE_HOLD_DURATION)
.with_long_press(theme::ERASE_HOLD_DURATION)
.initially_enabled(false);
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);
}
/// 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 {
self.textbox.is_empty()
}

View File

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