From 4eefaffac90773d41f5151ae67119a983f7af9d9 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Fri, 4 Mar 2022 12:56:49 +0100 Subject: [PATCH] fix(core/rust/ui): recovery/passphrase keyboard fixes [no changelog] --- core/embed/extmod/rustmods/modtrezorui2.c | 31 ++++ core/embed/rust/librust.h | 6 + core/embed/rust/librust_qstr.h | 1 + core/embed/rust/src/ui/component/maybe.rs | 5 + core/embed/rust/src/ui/geometry.rs | 6 +- core/embed/rust/src/ui/layout/obj.rs | 3 +- .../rust/src/ui/model_tt/component/button.rs | 4 + .../ui/model_tt/component/keyboard/bip39.rs | 17 +-- .../ui/model_tt/component/keyboard/common.rs | 24 ++- .../model_tt/component/keyboard/mnemonic.rs | 30 +++- .../model_tt/component/keyboard/passphrase.rs | 103 ++++++++++--- .../src/ui/model_tt/component/keyboard/pin.rs | 2 - .../ui/model_tt/component/keyboard/slip39.rs | 28 ++-- .../rust/src/ui/model_tt/component/mod.rs | 10 +- .../rust/src/ui/model_tt/component/page.rs | 119 +-------------- .../rust/src/ui/model_tt/component/scroll.rs | 138 ++++++++++++++++++ core/embed/rust/src/ui/model_tt/layout.rs | 63 +++++++- core/embed/rust/src/ui/model_tt/theme.rs | 10 +- core/mocks/generated/trezorui2.pyi | 27 +++- core/src/trezor/ui/layouts/tt_v2/__init__.py | 42 +++++- 20 files changed, 481 insertions(+), 188 deletions(-) create mode 100644 core/embed/rust/src/ui/model_tt/component/scroll.rs diff --git a/core/embed/extmod/rustmods/modtrezorui2.c b/core/embed/extmod/rustmods/modtrezorui2.c index 01ede0212..5dc05f1d0 100644 --- a/core/embed/extmod/rustmods/modtrezorui2.c +++ b/core/embed/extmod/rustmods/modtrezorui2.c @@ -53,6 +53,31 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj, /// """PIN keyboard.""" STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_pin_obj, 0, ui_layout_new_pin); + +/// def layout_new_passphrase( +/// *, +/// prompt: str, +/// max_len: int, +/// ) -> object: +/// """Passphrase keyboard.""" +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_passphrase_obj, 0, + ui_layout_new_passphrase); + +/// def layout_new_bip39( +/// *, +/// prompt: str, +/// ) -> object: +/// """BIP39 keyboard.""" +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_bip39_obj, 0, + ui_layout_new_bip39); + +/// def layout_new_slip39( +/// *, +/// prompt: str, +/// ) -> object: +/// """BIP39 keyboard.""" +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_slip39_obj, 0, + ui_layout_new_slip39); #elif TREZOR_MODEL == 1 /// def layout_new_confirm_text( /// *, @@ -75,6 +100,12 @@ STATIC const mp_rom_map_elem_t mp_module_trezorui2_globals_table[] = { MP_ROM_PTR(&mod_trezorui2_layout_new_example_obj)}, {MP_ROM_QSTR(MP_QSTR_layout_new_pin), MP_ROM_PTR(&mod_trezorui2_layout_new_pin_obj)}, + {MP_ROM_QSTR(MP_QSTR_layout_new_passphrase), + MP_ROM_PTR(&mod_trezorui2_layout_new_passphrase_obj)}, + {MP_ROM_QSTR(MP_QSTR_layout_new_bip39), + MP_ROM_PTR(&mod_trezorui2_layout_new_bip39_obj)}, + {MP_ROM_QSTR(MP_QSTR_layout_new_slip39), + MP_ROM_PTR(&mod_trezorui2_layout_new_slip39_obj)}, #elif TREZOR_MODEL == 1 {MP_ROM_QSTR(MP_QSTR_layout_new_confirm_text), MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_text_obj)}, diff --git a/core/embed/rust/librust.h b/core/embed/rust/librust.h index 2f4ecbe9a..26f0e7d14 100644 --- a/core/embed/rust/librust.h +++ b/core/embed/rust/librust.h @@ -19,6 +19,12 @@ mp_obj_t ui_layout_new_confirm_text(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs); mp_obj_t ui_layout_new_pin(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs); +mp_obj_t ui_layout_new_passphrase(size_t n_args, const mp_obj_t *args, + mp_map_t *kwargs); +mp_obj_t ui_layout_new_bip39(size_t n_args, const mp_obj_t *args, + mp_map_t *kwargs); +mp_obj_t ui_layout_new_slip39(size_t n_args, const mp_obj_t *args, + mp_map_t *kwargs); #ifdef TREZOR_EMULATOR mp_obj_t ui_debug_layout_type(); diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 2a44d1df8..751cb3470 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -29,4 +29,5 @@ static void _librust_qstrs(void) { MP_QSTR_subprompt; MP_QSTR_warning; MP_QSTR_allow_cancel; + MP_QSTR_max_len; } diff --git a/core/embed/rust/src/ui/component/maybe.rs b/core/embed/rust/src/ui/component/maybe.rs index 820fdd4f3..e6a44c32d 100644 --- a/core/embed/rust/src/ui/component/maybe.rs +++ b/core/embed/rust/src/ui/component/maybe.rs @@ -92,4 +92,9 @@ where self.inner.paint(); } } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.pad.area); + self.inner.bounds(sink); + } } diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index c884dcf45..f0d371cba 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -461,9 +461,9 @@ pub struct GridCellSpan { #[derive(Copy, Clone)] pub struct LinearPlacement { - axis: Axis, - align: Alignment, - spacing: i32, + pub axis: Axis, + pub align: Alignment, + pub spacing: i32, } impl LinearPlacement { diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 471d1117e..398ff7a90 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -259,7 +259,8 @@ impl LayoutObj { display::rect_stroke(r, color) } - // wireframe(display::screen()); + // use crate::ui::model_tt::theme; + // wireframe(theme::borders()); self.inner.borrow().root.obj_bounds(&mut wireframe); } diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index af688ef09..f3650fd93 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -257,6 +257,10 @@ where self.paint_background(&style); self.paint_content(&style); } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area); + } } #[cfg(feature = "ui_debug")] diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs index 3a6f7f2e4..f4525e0a8 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/bip39.rs @@ -7,7 +7,7 @@ use crate::{ model_tt::{ component::{ keyboard::{ - common::{MultiTapKeyboard, TextBox}, + common::{paint_pending_marker, MultiTapKeyboard, TextBox}, mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT}, }, Button, ButtonContent, ButtonMsg, @@ -118,16 +118,7 @@ impl Component for Bip39Input { // Paint the pending marker. if self.multi_tap.pending_key().is_some() { - // Measure the width of the last character of input. - if let Some(last) = text.last().copied() { - let last_width = style.font.text_width(&[last]); - // Draw the marker 2px under the start of the baseline of the last character. - let marker_origin = text_baseline + Offset::new(width - last_width, 2); - // Draw the marker 1px longer than the last character, and 3px thick. - let marker_rect = - Rect::from_top_left_and_size(marker_origin, Offset::new(last_width + 1, 3)); - display::rect_fill(marker_rect, style.text_color); - } + paint_pending_marker(text_baseline, text, style.font, style.text_color); } // Paint the icon. @@ -138,6 +129,10 @@ impl Component for Bip39Input { display::icon(icon_center, icon, style.text_color, style.button_color); } } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + self.button.bounds(sink); + } } impl Bip39Input { diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs index 7cd32a6f6..87b325750 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs @@ -2,10 +2,18 @@ use heapless::String; use crate::{ time::Duration, - ui::component::{Event, EventCtx, TimerToken}, + ui::{ + component::{Event, EventCtx, TimerToken}, + display::{self, Color, Font}, + geometry::{Offset, Point, Rect}, + }, util::ResultExt, }; +pub const HEADER_HEIGHT: i32 = 25; +pub const HEADER_PADDING_SIDE: i32 = 5; +pub const HEADER_PADDING_BOTTOM: i32 = 12; + /// Contains state commonly used in implementations multi-tap keyboards. pub struct MultiTapKeyboard { /// Configured timeout after which we cancel currently pending key. @@ -199,3 +207,17 @@ impl TextBox { } } } + +pub fn paint_pending_marker(text_baseline: Point, text: &[u8], font: Font, color: Color) { + // Measure the width of the last character of input. + if let Some(last) = text.last().copied() { + let width = font.text_width(text); + let last_width = font.text_width(&[last]); + // Draw the marker 2px under the start of the baseline of the last character. + let marker_origin = text_baseline + Offset::new(width - last_width, 2); + // Draw the marker 1px longer than the last character, and 3px thick. + let marker_rect = + Rect::from_top_left_and_size(marker_origin, Offset::new(last_width + 1, 3)); + display::rect_fill(marker_rect, color); + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs index 91bbf8f93..408f92f4d 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/mnemonic.rs @@ -1,6 +1,6 @@ use crate::ui::{ component::{Child, Component, Event, EventCtx, Label, Maybe}, - geometry::{Grid, Rect}, + geometry::{Alignment, Grid, Rect}, model_tt::{ component::{Button, ButtonMsg}, theme, @@ -32,7 +32,7 @@ where Self { prompt: Child::new(Maybe::visible( theme::BG, - Label::left_aligned(prompt, theme::label_default()), + Label::centered(prompt, theme::label_keyboard()), )), back: Child::new(Maybe::hidden( theme::BG, @@ -84,10 +84,15 @@ where type Msg = MnemonicKeyboardMsg; fn place(&mut self, bounds: Rect) -> Rect { - let grid = Grid::new(bounds, 3, 4); + let grid = + Grid::new(bounds.inset(theme::borders()), 4, 3).with_spacing(theme::KEYBOARD_SPACING); let back_area = grid.row_col(0, 0); let input_area = grid.row_col(0, 1).union(grid.row_col(0, 3)); - let prompt_area = grid.row_col(0, 0).union(grid.row_col(0, 3)); + + let prompt_center = grid.row_col(0, 0).union(grid.row_col(0, 3)).center(); + let prompt_size = self.prompt.inner().inner().size(); + let prompt_top_left = prompt_size.snap(prompt_center, Alignment::Center, Alignment::Center); + let prompt_area = Rect::from_top_left_and_size(prompt_top_left, prompt_size); self.prompt.place(prompt_area); self.back.place(back_area); @@ -136,6 +141,15 @@ where btn.paint(); } } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + self.prompt.bounds(sink); + self.input.bounds(sink); + self.back.bounds(sink); + for btn in &self.keys { + btn.bounds(sink) + } + } } pub trait MnemonicInput: Component { @@ -151,3 +165,11 @@ pub enum MnemonicInputMsg { Completed, TimedOut, } + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for MnemonicKeyboard { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("MnemonicKeyboard"); + t.close(); + } +} diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs index f85b34145..1a9e82a4e 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/passphrase.rs @@ -1,12 +1,12 @@ use crate::ui::{ component::{base::ComponentExt, Child, Component, Event, EventCtx, Never}, display, - geometry::{Grid, Rect}, + geometry::{Grid, Insets, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg::Clicked}, - keyboard::common::{MultiTapKeyboard, TextBox}, + keyboard::common::{paint_pending_marker, MultiTapKeyboard, TextBox, HEADER_PADDING_SIDE}, swipe::{Swipe, SwipeDirection}, - theme, + theme, ScrollBar, }, }; @@ -21,7 +21,8 @@ pub struct PassphraseKeyboard { back: Child>, confirm: Child>, keys: [[Child>; KEY_COUNT]; PAGE_COUNT], - key_page: usize, + scrollbar: ScrollBar, + fade: bool, } const STARTING_PAGE: usize = 1; @@ -42,11 +43,12 @@ impl PassphraseKeyboard { Self { page_swipe: Swipe::horizontal(), input: Input::new().into_child(), - confirm: Button::with_text("Confirm") + confirm: Button::with_icon(theme::ICON_CONFIRM) .styled(theme::button_confirm()) .into_child(), - back: Button::with_text("Back") - .styled(theme::button_clear()) + back: Button::with_icon(theme::ICON_BACK) + .styled(theme::button_reset()) + .initially_enabled(false) .into_child(), keys: KEYBOARD.map(|page| { page.map(|text| { @@ -58,7 +60,8 @@ impl PassphraseKeyboard { } }) }), - key_page: STARTING_PAGE, + scrollbar: ScrollBar::horizontal(), + fade: false, } } @@ -72,18 +75,22 @@ impl PassphraseKeyboard { fn on_page_swipe(&mut self, ctx: &mut EventCtx, swipe: SwipeDirection) { // Change the page number. - self.key_page = match swipe { - SwipeDirection::Left => (self.key_page as isize + 1) as usize % PAGE_COUNT, - SwipeDirection::Right => (self.key_page as isize - 1) as usize % PAGE_COUNT, - _ => self.key_page, + let key_page = self.scrollbar.active_page; + let key_page = match swipe { + SwipeDirection::Left => (key_page as isize + 1) as usize % PAGE_COUNT, + SwipeDirection::Right => (key_page as isize - 1) as usize % PAGE_COUNT, + _ => key_page, }; + self.scrollbar.go_to(key_page); // Clear the pending state. self.input .mutate(ctx, |ctx, i| i.multi_tap.clear_pending_state(ctx)); // Make sure to completely repaint the buttons. - for btn in &mut self.keys[self.key_page] { + for btn in &mut self.keys[key_page] { btn.request_complete_repaint(ctx); } + // Reset backlight to normal level on next paint. + self.fade = true; } fn after_edit(&mut self, ctx: &mut EventCtx) { @@ -99,16 +106,28 @@ impl Component for PassphraseKeyboard { type Msg = PassphraseKeyboardMsg; fn place(&mut self, bounds: Rect) -> Rect { - let input_area = Grid::new(bounds, 5, 1).row_col(0, 0); - let confirm_btn_area = Grid::new(bounds, 5, 3).cell(14); - let back_btn_area = Grid::new(bounds, 5, 3).cell(12); - let key_grid = Grid::new(bounds, 5, 3); + let bounds = bounds.inset(theme::borders()); + + let input_area = Grid::new(bounds, 5, 1) + .with_spacing(theme::KEYBOARD_SPACING) + .row_col(0, 0); + + let (input_area, scroll_area) = input_area.split_bottom(ScrollBar::DOT_SIZE); + let input_area = + input_area.inset(Insets::new(0, HEADER_PADDING_SIDE, 2, HEADER_PADDING_SIDE)); + + let key_grid = Grid::new(bounds, 5, 3).with_spacing(theme::KEYBOARD_SPACING); + let confirm_btn_area = key_grid.cell(14); + let back_btn_area = key_grid.cell(12); self.page_swipe.place(bounds); self.input.place(input_area); self.confirm.place(confirm_btn_area); self.back.place(back_btn_area); - for (key, btn) in self.keys[self.key_page].iter_mut().enumerate() { + self.scrollbar.place(scroll_area); + self.scrollbar + .set_count_and_active_page(PAGE_COUNT, STARTING_PAGE); + for (key, btn) in self.keys[self.scrollbar.active_page].iter_mut().enumerate() { // Assign the keys in each page to buttons on a 5x3 grid, starting from the // second row. let area = key_grid.cell(if key < 9 { @@ -152,7 +171,7 @@ impl Component for PassphraseKeyboard { None }; } - for (key, btn) in self.keys[self.key_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) { // 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. @@ -170,11 +189,27 @@ impl Component for PassphraseKeyboard { fn paint(&mut self) { self.input.paint(); + self.scrollbar.paint(); self.confirm.paint(); self.back.paint(); - for btn in &mut self.keys[self.key_page] { + for btn in &mut self.keys[self.scrollbar.active_page] { btn.paint(); } + if self.fade { + self.fade = false; + // Note that this is blocking and takes some time. + display::fade_backlight(theme::BACKLIGHT_NORMAL); + } + } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + self.input.bounds(sink); + self.scrollbar.bounds(sink); + self.confirm.bounds(sink); + self.back.bounds(sink); + for btn in &self.keys[self.scrollbar.active_page] { + btn.bounds(sink) + } } } @@ -207,14 +242,38 @@ impl Component for Input { } fn paint(&mut self) { + const TEXT_OFFSET: Offset = Offset::y(8); + let style = theme::label_default(); + let text_baseline = self.area.bottom_left() - TEXT_OFFSET; + let text = self.textbox.content().as_bytes(); + // Possible optimization is to redraw the background only when pending character + // is replaced, or only draw rectangle over the pending character and + // marker. + display::rect_fill(self.area, theme::BG); display::text( - self.area.bottom_left(), - self.textbox.content().as_bytes(), + text_baseline, + text, style.font, style.text_color, style.background_color, ); + // Paint the pending marker. + if self.multi_tap.pending_key().is_some() { + paint_pending_marker(text_baseline, text, style.font, style.text_color); + } + } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area) + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for PassphraseKeyboard { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("PassphraseKeyboard"); + t.close(); } } diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs index 56316e59f..d244562c7 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs @@ -42,7 +42,6 @@ impl PinKeyboard where T: Deref, { - const BUTTON_SPACING: i32 = 8; const HEADER_HEIGHT: i32 = 25; const HEADER_PADDING_SIDE: i32 = 5; const HEADER_PADDING_BOTTOM: i32 = 12; @@ -57,7 +56,6 @@ where major_warning: Option, allow_cancel: bool, ) -> Self { - let area = area.inset(Insets::right(theme::CONTENT_BORDER)); let digits = Vec::new(); // Control buttons. diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs index ded5d2b7a..d6cfcc83f 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/slip39.rs @@ -11,7 +11,7 @@ use crate::{ model_tt::{ component::{ keyboard::{ - common::{MultiTapKeyboard, TextBox, TextEdit}, + common::{paint_pending_marker, MultiTapKeyboard, TextBox, TextEdit}, mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT}, }, Button, ButtonContent, ButtonMsg, @@ -36,7 +36,7 @@ impl MnemonicInput for Slip39Input { /// Return the key set. Keys are further specified as indices into this /// array. fn keys() -> [&'static str; MNEMONIC_KEY_COUNT] { - ["ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "xyz"] + ["ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz"] } /// Returns `true` if given key index can continue towards a valid mnemonic @@ -64,6 +64,8 @@ impl MnemonicInput for Slip39Input { // Ignore the pending char rotation. We use the pending key to paint // the last character, but the mnemonic word computation depends // only on the pressed key, not on the specific character inside it. + // Request paint of pending char. + ctx.request_paint(); } self.complete_word_from_dictionary(ctx); } @@ -147,20 +149,10 @@ impl Component for Slip39Input { style.text_color, style.button_color, ); - let width = style.font.text_width(text.as_bytes()); // Paint the pending marker. if self.multi_tap.pending_key().is_some() && self.final_word.is_none() { - // Measure the width of the last character of input. - if let Some(last) = text.as_bytes().last().copied() { - let last_width = style.font.text_width(&[last]); - // Draw the marker 2px under the start of the baseline of the last character. - let marker_origin = text_baseline + Offset::new(width - last_width, 2); - // Draw the marker 1px longer than the last character, and 3px thick. - let marker_rect = - Rect::from_top_left_and_size(marker_origin, Offset::new(last_width + 1, 3)); - display::rect_fill(marker_rect, style.text_color); - } + paint_pending_marker(text_baseline, text.as_bytes(), style.font, style.text_color); } // Paint the icon. @@ -171,6 +163,10 @@ impl Component for Slip39Input { display::icon(icon_center, icon, style.text_color, style.button_color); } } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + self.button.bounds(sink); + } } impl Slip39Input { @@ -200,11 +196,15 @@ impl Slip39Input { fn complete_word_from_dictionary(&mut self, ctx: &mut EventCtx) { let sequence = self.input_sequence(); - self.final_word = sequence.and_then(slip39::button_sequence_to_word); self.input_mask = sequence .and_then(slip39::word_completion_mask) .map(Slip39Mask) .unwrap_or_else(Slip39Mask::full); + self.final_word = if self.input_mask.is_final() { + sequence.and_then(slip39::button_sequence_to_word) + } else { + None + }; // Change the style of the button depending on the input. if self.final_word.is_some() { diff --git a/core/embed/rust/src/ui/model_tt/component/mod.rs b/core/embed/rust/src/ui/model_tt/component/mod.rs index 60ba2b670..1e61cc321 100644 --- a/core/embed/rust/src/ui/model_tt/component/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/mod.rs @@ -5,15 +5,23 @@ mod frame; mod keyboard; mod loader; mod page; +mod scroll; mod swipe; pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet}; pub use confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use dialog::{Dialog, DialogLayout, DialogMsg}; pub use frame::Frame; -pub use keyboard::pin::{PinKeyboard, PinKeyboardMsg}; +pub use keyboard::{ + bip39::Bip39Input, + mnemonic::{MnemonicKeyboard, MnemonicKeyboardMsg}, + passphrase::{PassphraseKeyboard, PassphraseKeyboardMsg}, + pin::{PinKeyboard, PinKeyboardMsg}, + slip39::Slip39Input, +}; pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use page::SwipePage; +pub use scroll::ScrollBar; pub use swipe::{Swipe, SwipeDirection}; use super::{event, theme}; diff --git a/core/embed/rust/src/ui/model_tt/component/page.rs b/core/embed/rust/src/ui/model_tt/component/page.rs index 6e8512880..7115e5bdf 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -1,12 +1,12 @@ use crate::ui::{ component::{ - base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, Never, Pad, Paginate, + base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, Pad, Paginate, }, display::{self, Color}, - geometry::{LinearPlacement, Offset, Rect}, + geometry::{Offset, Rect}, }; -use super::{theme, Button, Swipe, SwipeDirection}; +use super::{theme, Button, ScrollBar, Swipe, SwipeDirection}; pub struct SwipePage { content: T, @@ -151,8 +151,8 @@ where } fn bounds(&self, sink: &mut dyn FnMut(Rect)) { - sink(self.scrollbar.area); sink(self.pad.area); + self.scrollbar.bounds(sink); self.content.bounds(sink); if !self.scrollbar.has_next_page() { self.buttons.bounds(sink); @@ -176,117 +176,6 @@ where } } -pub struct ScrollBar { - area: Rect, - page_count: usize, - active_page: usize, -} - -impl ScrollBar { - const DOT_SIZE: i32 = 6; - /// Edge to edge. - const DOT_INTERVAL: i32 = 6; - /// Edge of last dot to center of arrow icon. - const ARROW_SPACE: i32 = 26; - - const ICON_UP: &'static [u8] = include_res!("model_tt/res/scroll-up.toif"); - const ICON_DOWN: &'static [u8] = include_res!("model_tt/res/scroll-down.toif"); - - pub fn vertical() -> Self { - Self { - area: Rect::zero(), - page_count: 0, - active_page: 0, - } - } - - pub fn set_count_and_active_page(&mut self, page_count: usize, active_page: usize) { - self.page_count = page_count; - self.active_page = active_page; - } - - pub fn has_pages(&self) -> bool { - self.page_count > 1 - } - - pub fn has_next_page(&self) -> bool { - self.active_page < self.page_count - 1 - } - - pub fn has_previous_page(&self) -> bool { - self.active_page > 0 - } - - pub fn go_to_next_page(&mut self) { - self.go_to(self.active_page.saturating_add(1).min(self.page_count - 1)); - } - - pub fn go_to_previous_page(&mut self) { - self.go_to(self.active_page.saturating_sub(1)); - } - - pub fn go_to(&mut self, active_page: usize) { - self.active_page = active_page; - } -} - -impl Component for ScrollBar { - type Msg = Never; - - fn place(&mut self, bounds: Rect) -> Rect { - self.area = bounds; - self.area - } - - fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { - None - } - - fn paint(&mut self) { - let layout = LinearPlacement::vertical() - .align_at_center() - .with_spacing(Self::DOT_INTERVAL); - - let mut i = 0; - let mut top = None; - let mut display_icon = |top_left| { - let icon = if i == self.active_page { - theme::DOT_ACTIVE - } else { - theme::DOT_INACTIVE - }; - display::icon_top_left(top_left, icon, theme::FG, theme::BG); - i += 1; - top.get_or_insert(top_left.x); - }; - - layout.arrange_uniform( - self.area, - self.page_count, - Offset::new(Self::DOT_SIZE, Self::DOT_SIZE), - &mut display_icon, - ); - - let arrow_distance = self.area.center().x - top.unwrap_or(0) + Self::ARROW_SPACE; - if self.has_previous_page() { - display::icon( - self.area.center() - Offset::y(arrow_distance), - Self::ICON_UP, - theme::FG, - theme::BG, - ); - } - if self.has_next_page() { - display::icon( - self.area.center() + Offset::y(arrow_distance), - Self::ICON_DOWN, - theme::FG, - theme::BG, - ); - } - } -} - pub struct PageLayout { pub content_single_page: Rect, pub content: Rect, diff --git a/core/embed/rust/src/ui/model_tt/component/scroll.rs b/core/embed/rust/src/ui/model_tt/component/scroll.rs new file mode 100644 index 000000000..7edb95f42 --- /dev/null +++ b/core/embed/rust/src/ui/model_tt/component/scroll.rs @@ -0,0 +1,138 @@ +use crate::ui::{ + component::{Component, Event, EventCtx, Never}, + display, + geometry::{LinearPlacement, Offset, Rect}, +}; + +use super::theme; + +pub struct ScrollBar { + area: Rect, + layout: LinearPlacement, + arrows: bool, + pub page_count: usize, + pub active_page: usize, +} + +impl ScrollBar { + pub const DOT_SIZE: i32 = 6; + /// Edge to edge. + const DOT_INTERVAL: i32 = 6; + /// Edge of last dot to center of arrow icon. + const ARROW_SPACE: i32 = 26; + + const ICON_UP: &'static [u8] = include_res!("model_tt/res/scroll-up.toif"); + const ICON_DOWN: &'static [u8] = include_res!("model_tt/res/scroll-down.toif"); + + fn new(layout: LinearPlacement) -> Self { + Self { + area: Rect::zero(), + layout: layout.align_at_center().with_spacing(Self::DOT_INTERVAL), + arrows: false, + page_count: 0, + active_page: 0, + } + } + + pub fn vertical() -> Self { + Self::new(LinearPlacement::vertical()) + } + + pub fn horizontal() -> Self { + Self::new(LinearPlacement::horizontal()) + } + + pub fn with_arrows(mut self) -> Self { + self.arrows = true; + self + } + + pub fn set_count_and_active_page(&mut self, page_count: usize, active_page: usize) { + self.page_count = page_count; + self.active_page = active_page; + } + + pub fn has_pages(&self) -> bool { + self.page_count > 1 + } + + pub fn has_next_page(&self) -> bool { + self.active_page < self.page_count - 1 + } + + pub fn has_previous_page(&self) -> bool { + self.active_page > 0 + } + + pub fn go_to_next_page(&mut self) { + self.go_to(self.active_page.saturating_add(1).min(self.page_count - 1)); + } + + pub fn go_to_previous_page(&mut self) { + self.go_to(self.active_page.saturating_sub(1)); + } + + pub fn go_to(&mut self, active_page: usize) { + self.active_page = active_page; + } +} + +impl Component for ScrollBar { + type Msg = Never; + + fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { + None + } + + fn paint(&mut self) { + let mut i = 0; + let mut top = None; + let mut display_icon = |top_left| { + let icon = if i == self.active_page { + theme::DOT_ACTIVE + } else { + theme::DOT_INACTIVE + }; + display::icon_top_left(top_left, icon, theme::FG, theme::BG); + i += 1; + top.get_or_insert(top_left.x); + }; + + self.layout.arrange_uniform( + self.area, + self.page_count, + Offset::new(Self::DOT_SIZE, Self::DOT_SIZE), + &mut display_icon, + ); + + if self.arrows { + let arrow_distance = self.area.center().x - top.unwrap_or(0) + Self::ARROW_SPACE; + let offset = Offset::on_axis(self.layout.axis, arrow_distance); + if self.has_previous_page() { + display::icon( + self.area.center() - offset, + Self::ICON_UP, + theme::FG, + theme::BG, + ); + } + if self.has_next_page() { + display::icon( + self.area.center() + offset, + Self::ICON_DOWN, + theme::FG, + theme::BG, + ); + } + } + } + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + bounds + } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area); + } +} diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 7a54ab49a..ea40d8eec 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -12,10 +12,11 @@ use crate::{ use super::{ component::{ - Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, PinKeyboard, - PinKeyboardMsg, SwipePage, + Bip39Input, Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, + MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard, PassphraseKeyboardMsg, + PinKeyboard, PinKeyboardMsg, Slip39Input, SwipePage, }, - constant, theme, + theme, }; impl TryFrom> for Obj @@ -62,6 +63,27 @@ impl TryFrom for Obj { } } +impl TryFrom for Obj { + type Error = Error; + + fn try_from(val: MnemonicKeyboardMsg) -> Result { + match val { + MnemonicKeyboardMsg::Confirmed => Ok(Obj::const_true()), + } + } +} + +impl TryFrom for Obj { + type Error = Error; + + fn try_from(val: PassphraseKeyboardMsg) -> Result { + match val { + PassphraseKeyboardMsg::Confirmed => Ok(Obj::const_true()), + PassphraseKeyboardMsg::Cancelled => Ok(Obj::const_none()), + } + } +} + #[no_mangle] extern "C" fn ui_layout_new_example(_param: Obj) -> Obj { let block = move || { @@ -138,6 +160,41 @@ extern "C" fn ui_layout_new_pin(n_args: usize, args: *const Obj, kwargs: *const unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +#[no_mangle] +extern "C" fn ui_layout_new_passphrase(n_args: usize, args: *const Obj, kwargs: *const Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let _prompt: Buffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; + let _max_len: u32 = kwargs.get(Qstr::MP_QSTR_max_len)?.try_into()?; + let obj = LayoutObj::new(PassphraseKeyboard::new().into_child())?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +#[no_mangle] +extern "C" fn ui_layout_new_bip39(n_args: usize, args: *const Obj, kwargs: *const Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let _prompt: Buffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; + let obj = LayoutObj::new( + MnemonicKeyboard::new(Bip39Input::new(), b"Type word 11 of 12").into_child(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + +#[no_mangle] +extern "C" fn ui_layout_new_slip39(n_args: usize, args: *const Obj, kwargs: *const Map) -> Obj { + let block = move |_args: &[Obj], kwargs: &Map| { + let _prompt: Buffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?; + let obj = LayoutObj::new( + MnemonicKeyboard::new(Slip39Input::new(), b"Type word 13 of 20").into_child(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/core/embed/rust/src/ui/model_tt/theme.rs b/core/embed/rust/src/ui/model_tt/theme.rs index e375f4167..3180a722e 100644 --- a/core/embed/rust/src/ui/model_tt/theme.rs +++ b/core/embed/rust/src/ui/model_tt/theme.rs @@ -4,9 +4,7 @@ use crate::ui::{ geometry::Insets, }; -use super::{ - component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet}, -}; +use super::component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet}; // Font constants. pub const FONT_NORMAL: Font = Font::new(-1); @@ -101,8 +99,8 @@ pub fn button_default() -> ButtonStyleSheet { }, active: &ButtonStyle { font: FONT_BOLD, - text_color: BG, - button_color: FG, + text_color: FG, + button_color: GREY_MEDIUM, background_color: BG, border_color: FG, border_radius: RADIUS, @@ -290,7 +288,7 @@ pub const KEYBOARD_SPACING: i32 = 8; /// +----------+ /// | 13 | /// | +----+ | -/// |10| | 5| +/// |10| |10| /// | +----+ | /// | 14 | /// +----------+ diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index 9b26d9706..b31d07def 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -25,12 +25,37 @@ def layout_new_pin( *, prompt: str, subprompt: str, - danger: bool, allow_cancel: bool, + warning: str | None, ) -> object: """PIN keyboard.""" +# extmod/rustmods/modtrezorui2.c +def layout_new_passphrase( + *, + prompt: str, + max_len: int, +) -> object: + """Passphrase keyboard.""" + + +# extmod/rustmods/modtrezorui2.c +def layout_new_bip39( + *, + prompt: str, +) -> object: + """BIP39 keyboard.""" + + +# extmod/rustmods/modtrezorui2.c +def layout_new_slip39( + *, + prompt: str, +) -> object: + """BIP39 keyboard.""" + + # extmod/rustmods/modtrezorui2.c def layout_new_confirm_text( *, diff --git a/core/src/trezor/ui/layouts/tt_v2/__init__.py b/core/src/trezor/ui/layouts/tt_v2/__init__.py index 7edfc404d..3b0a2c71c 100644 --- a/core/src/trezor/ui/layouts/tt_v2/__init__.py +++ b/core/src/trezor/ui/layouts/tt_v2/__init__.py @@ -3,9 +3,15 @@ from typing import TYPE_CHECKING from trezor import io, log, loop, ui, wire, workflow from trezor.enums import ButtonRequestType -from trezorui2 import layout_new_confirm_action, layout_new_pin - -from ...components.tt import pin +from trezorui2 import ( + layout_new_bip39, + layout_new_confirm_action, + layout_new_passphrase, + layout_new_pin, + layout_new_slip39, +) + +from ...components.tt import passphrase, pin from ...constants.tt import MONO_ADDR_PER_LINE from ..common import button_request, interact @@ -443,7 +449,19 @@ def draw_simple_text(title: str, description: str = "") -> None: async def request_passphrase_on_device(ctx: wire.GenericContext, max_len: int) -> str: - raise NotImplementedError + await button_request( + ctx, "passphrase_device", code=ButtonRequestType.PassphraseEntry + ) + + keyboard = _RustLayout( + layout_new_passphrase(prompt="Enter passphrase", max_len=max_len) + ) + result = await ctx.wait(keyboard) + if result is passphrase.CANCELLED: + raise wire.ActionCancelled("Passphrase entry cancelled") + + assert isinstance(result, str) + return result async def request_pin_on_device( @@ -475,3 +493,19 @@ async def request_pin_on_device( raise wire.PinCancelled assert isinstance(result, str) return result + + +async def request_word( + ctx: wire.GenericContext, word_index: int, word_count: int, is_slip39: bool +) -> str: + if is_slip39: + keyboard: Any = _RustLayout( + layout_new_bip39(prompt=f"Type word {word_index + 1} of {word_count}:") + ) + else: + keyboard = _RustLayout( + layout_new_slip39(prompt=f"Type word {word_index + 1} of {word_count}:") + ) + + word: str = await ctx.wait(keyboard) + return word