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:
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 {
|
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.
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user