mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-28 15:22:14 +00:00
feat(eckhart): full-screen mnemonic component
This commit is contained in:
parent
89b05ae0d3
commit
93f900b734
@ -0,0 +1,283 @@
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx, Label, Maybe},
|
||||
geometry::Rect,
|
||||
shape::Renderer,
|
||||
},
|
||||
};
|
||||
|
||||
use super::super::super::{
|
||||
component::{
|
||||
button::ButtonContent,
|
||||
keyboard::{
|
||||
common::{INPUT_TOUCH_HEIGHT, KEYBOARD_INPUT_INSETS, KEYPAD_VISIBLE_HEIGHT},
|
||||
keypad::{ButtonState, Keypad, KeypadButton, KeypadMsg},
|
||||
},
|
||||
},
|
||||
constant::SCREEN,
|
||||
theme,
|
||||
};
|
||||
|
||||
pub const MNEMONIC_KEY_COUNT: usize = 9;
|
||||
|
||||
pub enum MnemonicKeyboardMsg {
|
||||
Confirmed,
|
||||
Previous,
|
||||
}
|
||||
|
||||
pub struct MnemonicKeyboard<T> {
|
||||
/// Initial prompt, displayed on empty input.
|
||||
prompt: Maybe<Label<'static>>,
|
||||
/// Input area, acting as the auto-complete.
|
||||
input: Maybe<T>,
|
||||
/// Key buttons.
|
||||
keypad: Keypad,
|
||||
/// Whether going back is allowed (is not on the very first word).
|
||||
can_go_back: bool,
|
||||
}
|
||||
|
||||
impl<T> MnemonicKeyboard<T>
|
||||
where
|
||||
T: MnemonicInput,
|
||||
{
|
||||
pub const KEY_COUNT: usize = 9;
|
||||
pub fn new(input: T, prompt: TString<'static>, can_go_back: bool) -> Self {
|
||||
// Input might be already pre-filled
|
||||
let prompt_visible = input.is_empty();
|
||||
|
||||
let keypad_content: [_; MNEMONIC_KEY_COUNT] =
|
||||
core::array::from_fn(|idx| ButtonContent::Text(T::keys()[idx].into()));
|
||||
|
||||
Self {
|
||||
prompt: Maybe::new(
|
||||
theme::BG,
|
||||
Label::centered(prompt, theme::TEXT_SMALL).vertically_centered(),
|
||||
prompt_visible,
|
||||
),
|
||||
keypad: Keypad::new_hidden().with_keys_content(&keypad_content),
|
||||
can_go_back,
|
||||
input: Maybe::new(theme::BG, input, !prompt_visible),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_input_change(&mut self, ctx: &mut EventCtx) {
|
||||
self.toggle_buttons(ctx);
|
||||
self.toggle_prompt_or_input(ctx);
|
||||
}
|
||||
|
||||
/// Either enable or disable the key buttons, depending on the dictionary
|
||||
/// completion mask and the pending key.
|
||||
fn toggle_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
let input = self.input.inner();
|
||||
// Enable/disable the key buttons based on the ability to form a valid word.
|
||||
for idx in 0..Self::KEY_COUNT {
|
||||
let state = if input.can_key_press_lead_to_a_valid_word(idx) {
|
||||
ButtonState::Enabled
|
||||
} else {
|
||||
ButtonState::Disabled
|
||||
};
|
||||
self.keypad
|
||||
.set_button_state(ctx, KeypadButton::Key(idx), &state);
|
||||
}
|
||||
|
||||
// Determine states for erase and back buttons
|
||||
let (erase_state, back_state) = if input.is_empty() {
|
||||
(
|
||||
ButtonState::Hidden,
|
||||
if self.can_go_back {
|
||||
ButtonState::Enabled
|
||||
} else {
|
||||
ButtonState::Hidden
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(ButtonState::Enabled, ButtonState::Hidden)
|
||||
};
|
||||
|
||||
// Determine state and style for the confirm button based on input state
|
||||
let confirm_state = if input.is_empty() || input.mnemonic().is_none() {
|
||||
ButtonState::Hidden
|
||||
} else {
|
||||
ButtonState::Enabled
|
||||
};
|
||||
let confirm_style = if input.mnemonic().is_some() {
|
||||
let any_press_can_lead_to_valid_word = || {
|
||||
(0..Self::KEY_COUNT)
|
||||
.any(|idx| self.input.inner().can_key_press_lead_to_a_valid_word(idx))
|
||||
};
|
||||
if any_press_can_lead_to_valid_word() {
|
||||
theme::button_keyboard()
|
||||
} else {
|
||||
theme::button_keyboard_confirm()
|
||||
}
|
||||
} else {
|
||||
theme::button_keyboard()
|
||||
};
|
||||
|
||||
// Apply all button states
|
||||
self.keypad
|
||||
.set_button_state(ctx, KeypadButton::Erase, &erase_state);
|
||||
self.keypad
|
||||
.set_button_state(ctx, KeypadButton::Back, &back_state);
|
||||
self.keypad
|
||||
.set_button_state(ctx, KeypadButton::Confirm, &confirm_state);
|
||||
|
||||
// Apply the stylesheet for the confirm button
|
||||
self.keypad
|
||||
.set_button_stylesheet(KeypadButton::Confirm, confirm_style);
|
||||
}
|
||||
|
||||
/// After edit operations, we need to either show or hide the prompt, the
|
||||
/// input, the erase button and the back button.
|
||||
fn toggle_prompt_or_input(&mut self, ctx: &mut EventCtx) {
|
||||
let input_empty = self.input.inner().is_empty();
|
||||
// Prompt is shown if the input is empty.
|
||||
self.prompt.show_if(ctx, input_empty);
|
||||
self.input.show_if(ctx, !input_empty);
|
||||
}
|
||||
|
||||
pub fn mnemonic(&self) -> Option<&'static str> {
|
||||
self.input.inner().mnemonic()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for MnemonicKeyboard<T>
|
||||
where
|
||||
T: MnemonicInput,
|
||||
{
|
||||
type Msg = MnemonicKeyboardMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// assert full screen
|
||||
debug_assert_eq!(bounds.height(), SCREEN.height());
|
||||
debug_assert_eq!(bounds.width(), SCREEN.width());
|
||||
|
||||
// Keypad and input areas are overlapped
|
||||
let (_, keypad_area) = bounds.split_bottom(KEYPAD_VISIBLE_HEIGHT);
|
||||
let (input_area, _) = bounds.split_top(INPUT_TOUCH_HEIGHT);
|
||||
|
||||
let prompt_area = input_area.inset(KEYBOARD_INPUT_INSETS);
|
||||
let input_area = input_area.inset(KEYBOARD_INPUT_INSETS);
|
||||
|
||||
// Prompt/input placement
|
||||
self.prompt.place(prompt_area);
|
||||
self.input.place(input_area);
|
||||
|
||||
// Keypad placement
|
||||
self.keypad.place(keypad_area);
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Attach(_) => {
|
||||
self.on_input_change(ctx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match self.input.event(ctx, event) {
|
||||
Some(MnemonicInputMsg::Confirmed) => {
|
||||
// Confirmed, bubble up.
|
||||
return Some(MnemonicKeyboardMsg::Confirmed);
|
||||
}
|
||||
Some(_) => {
|
||||
// Either a timeout or a completion.
|
||||
self.on_input_change(ctx);
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match self.keypad.event(ctx, event) {
|
||||
Some(KeypadMsg::Key(idx)) => {
|
||||
self.input.inner_mut().on_key_click(ctx, idx);
|
||||
self.on_input_change(ctx);
|
||||
return None;
|
||||
}
|
||||
Some(KeypadMsg::Back) => {
|
||||
// Back button will cause going back to the previous word when allowed.
|
||||
if self.can_go_back {
|
||||
return Some(MnemonicKeyboardMsg::Previous);
|
||||
}
|
||||
}
|
||||
Some(KeypadMsg::EraseShort) => {
|
||||
self.input.inner_mut().on_backspace_click(ctx);
|
||||
self.on_input_change(ctx);
|
||||
return None;
|
||||
}
|
||||
Some(KeypadMsg::EraseLong) => {
|
||||
self.input.inner_mut().on_backspace_long_press(ctx);
|
||||
self.on_input_change(ctx);
|
||||
return None;
|
||||
}
|
||||
Some(KeypadMsg::Confirm) => {
|
||||
match self.input.inner_mut().on_confirm_click(ctx) {
|
||||
Some(MnemonicInputMsg::Confirmed) => {
|
||||
// Confirmed, bubble up.
|
||||
return Some(MnemonicKeyboardMsg::Confirmed);
|
||||
}
|
||||
Some(_) => {
|
||||
// Either a timeout or a completion.
|
||||
self.on_input_change(ctx);
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
let render_prompt_or_input = |target| {
|
||||
if self.input.inner().is_empty() {
|
||||
self.prompt.render(target);
|
||||
} else {
|
||||
self.input.render(target);
|
||||
}
|
||||
};
|
||||
|
||||
if self.keypad.pressed() {
|
||||
render_prompt_or_input(target);
|
||||
self.keypad.render(target);
|
||||
} else {
|
||||
self.keypad.render(target);
|
||||
render_prompt_or_input(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MnemonicInput: Component<Msg = MnemonicInputMsg> {
|
||||
fn keys() -> [&'static str; MNEMONIC_KEY_COUNT];
|
||||
fn can_key_press_lead_to_a_valid_word(&self, key: usize) -> bool;
|
||||
fn can_be_confirmed(&self) -> bool;
|
||||
fn on_key_click(&mut self, ctx: &mut EventCtx, key: usize);
|
||||
fn on_backspace_click(&mut self, ctx: &mut EventCtx);
|
||||
fn on_confirm_click(&mut self, ctx: &mut EventCtx) -> Option<MnemonicInputMsg>;
|
||||
fn on_backspace_long_press(&mut self, ctx: &mut EventCtx);
|
||||
fn is_empty(&self) -> bool;
|
||||
fn mnemonic(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
pub enum MnemonicInputMsg {
|
||||
Confirmed,
|
||||
Completed,
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for MnemonicKeyboard<T>
|
||||
where
|
||||
T: MnemonicInput + crate::trace::Trace,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("MnemonicKeyboard");
|
||||
t.child("prompt", &self.prompt);
|
||||
t.child("input", &self.input);
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub mod mnemonic;
|
||||
pub mod passphrase;
|
||||
pub mod pin;
|
||||
|
||||
|
@ -22,6 +22,7 @@ pub use hint::Hint;
|
||||
pub use hold_to_confirm::HoldToConfirmAnim;
|
||||
#[cfg(feature = "translations")]
|
||||
pub use keyboard::{
|
||||
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},
|
||||
passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg},
|
||||
pin::{PinKeyboard, PinKeyboardMsg},
|
||||
};
|
||||
|
@ -14,8 +14,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::component::{
|
||||
AllowedTextContent, PinKeyboard, PinKeyboardMsg, SelectWordMsg, SelectWordScreen, TextScreen,
|
||||
TextScreenMsg,
|
||||
AllowedTextContent, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PinKeyboard,
|
||||
PinKeyboardMsg, SelectWordMsg, SelectWordScreen, TextScreen, TextScreenMsg,
|
||||
};
|
||||
|
||||
impl ComponentMsgObj for PinKeyboard<'_> {
|
||||
@ -27,6 +27,24 @@ impl ComponentMsgObj for PinKeyboard<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ComponentMsgObj for MnemonicKeyboard<T>
|
||||
where
|
||||
T: MnemonicInput,
|
||||
{
|
||||
fn msg_try_into_obj(&self, msg: Self::Msg) -> Result<Obj, Error> {
|
||||
match msg {
|
||||
MnemonicKeyboardMsg::Confirmed => {
|
||||
if let Some(word) = self.mnemonic() {
|
||||
word.try_into()
|
||||
} else {
|
||||
fatal_error!("Invalid mnemonic")
|
||||
}
|
||||
}
|
||||
MnemonicKeyboardMsg::Previous => "".try_into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clippy/compiler complains about conflicting implementations
|
||||
// TODO move the common impls to a common module
|
||||
#[cfg(not(feature = "clippy"))]
|
||||
|
@ -25,7 +25,8 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
component::{
|
||||
ActionBar, Button, Header, HeaderMsg, Hint, PinKeyboard, SelectWordScreen, TextScreen,
|
||||
ActionBar, Button, Header, HeaderMsg, Hint, MnemonicKeyboard, PinKeyboard,
|
||||
SelectWordScreen, TextScreen,
|
||||
},
|
||||
flow, fonts, theme, UIEckhart,
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user