mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-26 16:18:22 +00:00
fix(core/rust): optimize keyboard behavior
[no changelog]
This commit is contained in:
parent
fc3ee87c25
commit
387af03842
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user