1
0
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:
Lukas Bielesch 2025-01-29 18:34:25 +01:00 committed by obrusvit
parent 89b05ae0d3
commit 93f900b734
5 changed files with 307 additions and 3 deletions

View File

@ -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);
}
}

View File

@ -1,3 +1,4 @@
pub mod mnemonic;
pub mod passphrase; pub mod passphrase;
pub mod pin; pub mod pin;

View File

@ -22,6 +22,7 @@ pub use hint::Hint;
pub use hold_to_confirm::HoldToConfirmAnim; pub use hold_to_confirm::HoldToConfirmAnim;
#[cfg(feature = "translations")] #[cfg(feature = "translations")]
pub use keyboard::{ pub use keyboard::{
mnemonic::{MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg},
passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg}, passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg},
pin::{PinKeyboard, PinKeyboardMsg}, pin::{PinKeyboard, PinKeyboardMsg},
}; };

View File

@ -14,8 +14,8 @@ use crate::{
}; };
use super::component::{ use super::component::{
AllowedTextContent, PinKeyboard, PinKeyboardMsg, SelectWordMsg, SelectWordScreen, TextScreen, AllowedTextContent, MnemonicInput, MnemonicKeyboard, MnemonicKeyboardMsg, PinKeyboard,
TextScreenMsg, PinKeyboardMsg, SelectWordMsg, SelectWordScreen, TextScreen, TextScreenMsg,
}; };
impl ComponentMsgObj for PinKeyboard<'_> { 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 // Clippy/compiler complains about conflicting implementations
// TODO move the common impls to a common module // TODO move the common impls to a common module
#[cfg(not(feature = "clippy"))] #[cfg(not(feature = "clippy"))]

View File

@ -25,7 +25,8 @@ use crate::{
use super::{ use super::{
component::{ component::{
ActionBar, Button, Header, HeaderMsg, Hint, PinKeyboard, SelectWordScreen, TextScreen, ActionBar, Button, Header, HeaderMsg, Hint, MnemonicKeyboard, PinKeyboard,
SelectWordScreen, TextScreen,
}, },
flow, fonts, theme, UIEckhart, flow, fonts, theme, UIEckhart,
}; };