1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-26 01:18:28 +00:00

fix(core/rust/ui): recovery/passphrase keyboard fixes

[no changelog]
This commit is contained in:
Martin Milata 2022-03-04 12:56:49 +01:00
parent efe25a6ab4
commit 4eefaffac9
20 changed files with 481 additions and 188 deletions

View File

@ -53,6 +53,31 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj,
/// """PIN keyboard.""" /// """PIN keyboard."""
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_pin_obj, 0, STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_pin_obj, 0,
ui_layout_new_pin); 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 #elif TREZOR_MODEL == 1
/// def layout_new_confirm_text( /// 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_PTR(&mod_trezorui2_layout_new_example_obj)},
{MP_ROM_QSTR(MP_QSTR_layout_new_pin), {MP_ROM_QSTR(MP_QSTR_layout_new_pin),
MP_ROM_PTR(&mod_trezorui2_layout_new_pin_obj)}, 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 #elif TREZOR_MODEL == 1
{MP_ROM_QSTR(MP_QSTR_layout_new_confirm_text), {MP_ROM_QSTR(MP_QSTR_layout_new_confirm_text),
MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_text_obj)}, MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_text_obj)},

View File

@ -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_map_t *kwargs);
mp_obj_t ui_layout_new_pin(size_t n_args, const mp_obj_t *args, mp_obj_t ui_layout_new_pin(size_t n_args, const mp_obj_t *args,
mp_map_t *kwargs); 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 #ifdef TREZOR_EMULATOR
mp_obj_t ui_debug_layout_type(); mp_obj_t ui_debug_layout_type();

View File

@ -29,4 +29,5 @@ static void _librust_qstrs(void) {
MP_QSTR_subprompt; MP_QSTR_subprompt;
MP_QSTR_warning; MP_QSTR_warning;
MP_QSTR_allow_cancel; MP_QSTR_allow_cancel;
MP_QSTR_max_len;
} }

View File

@ -92,4 +92,9 @@ where
self.inner.paint(); self.inner.paint();
} }
} }
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.pad.area);
self.inner.bounds(sink);
}
} }

View File

@ -461,9 +461,9 @@ pub struct GridCellSpan {
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct LinearPlacement { pub struct LinearPlacement {
axis: Axis, pub axis: Axis,
align: Alignment, pub align: Alignment,
spacing: i32, pub spacing: i32,
} }
impl LinearPlacement { impl LinearPlacement {

View File

@ -259,7 +259,8 @@ impl LayoutObj {
display::rect_stroke(r, color) 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); self.inner.borrow().root.obj_bounds(&mut wireframe);
} }

View File

@ -257,6 +257,10 @@ where
self.paint_background(&style); self.paint_background(&style);
self.paint_content(&style); self.paint_content(&style);
} }
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area);
}
} }
#[cfg(feature = "ui_debug")] #[cfg(feature = "ui_debug")]

View File

@ -7,7 +7,7 @@ use crate::{
model_tt::{ model_tt::{
component::{ component::{
keyboard::{ keyboard::{
common::{MultiTapKeyboard, TextBox}, common::{paint_pending_marker, MultiTapKeyboard, TextBox},
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT}, mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
}, },
Button, ButtonContent, ButtonMsg, Button, ButtonContent, ButtonMsg,
@ -118,16 +118,7 @@ impl Component for Bip39Input {
// Paint the pending marker. // Paint the pending marker.
if self.multi_tap.pending_key().is_some() { if self.multi_tap.pending_key().is_some() {
// Measure the width of the last character of input. paint_pending_marker(text_baseline, text, style.font, style.text_color);
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 the icon. // Paint the icon.
@ -138,6 +129,10 @@ impl Component for Bip39Input {
display::icon(icon_center, icon, style.text_color, style.button_color); 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 { impl Bip39Input {

View File

@ -2,10 +2,18 @@ use heapless::String;
use crate::{ use crate::{
time::Duration, time::Duration,
ui::component::{Event, EventCtx, TimerToken}, ui::{
component::{Event, EventCtx, TimerToken},
display::{self, Color, Font},
geometry::{Offset, Point, Rect},
},
util::ResultExt, 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. /// Contains state commonly used in implementations multi-tap keyboards.
pub struct MultiTapKeyboard { pub struct MultiTapKeyboard {
/// Configured timeout after which we cancel currently pending key. /// Configured timeout after which we cancel currently pending key.
@ -199,3 +207,17 @@ impl<const L: usize> TextBox<L> {
} }
} }
} }
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);
}
}

View File

@ -1,6 +1,6 @@
use crate::ui::{ use crate::ui::{
component::{Child, Component, Event, EventCtx, Label, Maybe}, component::{Child, Component, Event, EventCtx, Label, Maybe},
geometry::{Grid, Rect}, geometry::{Alignment, Grid, Rect},
model_tt::{ model_tt::{
component::{Button, ButtonMsg}, component::{Button, ButtonMsg},
theme, theme,
@ -32,7 +32,7 @@ where
Self { Self {
prompt: Child::new(Maybe::visible( prompt: Child::new(Maybe::visible(
theme::BG, theme::BG,
Label::left_aligned(prompt, theme::label_default()), Label::centered(prompt, theme::label_keyboard()),
)), )),
back: Child::new(Maybe::hidden( back: Child::new(Maybe::hidden(
theme::BG, theme::BG,
@ -84,10 +84,15 @@ where
type Msg = MnemonicKeyboardMsg; type Msg = MnemonicKeyboardMsg;
fn place(&mut self, bounds: Rect) -> Rect { 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 back_area = grid.row_col(0, 0);
let input_area = grid.row_col(0, 1).union(grid.row_col(0, 3)); 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.prompt.place(prompt_area);
self.back.place(back_area); self.back.place(back_area);
@ -136,6 +141,15 @@ where
btn.paint(); 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<Msg = MnemonicInputMsg> { pub trait MnemonicInput: Component<Msg = MnemonicInputMsg> {
@ -151,3 +165,11 @@ pub enum MnemonicInputMsg {
Completed, Completed,
TimedOut, TimedOut,
} }
#[cfg(feature = "ui_debug")]
impl<T> crate::trace::Trace for MnemonicKeyboard<T> {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("MnemonicKeyboard");
t.close();
}
}

View File

@ -1,12 +1,12 @@
use crate::ui::{ use crate::ui::{
component::{base::ComponentExt, Child, Component, Event, EventCtx, Never}, component::{base::ComponentExt, Child, Component, Event, EventCtx, Never},
display, display,
geometry::{Grid, Rect}, geometry::{Grid, Insets, Offset, Rect},
model_tt::component::{ model_tt::component::{
button::{Button, ButtonContent, ButtonMsg::Clicked}, button::{Button, ButtonContent, ButtonMsg::Clicked},
keyboard::common::{MultiTapKeyboard, TextBox}, keyboard::common::{paint_pending_marker, MultiTapKeyboard, TextBox, HEADER_PADDING_SIDE},
swipe::{Swipe, SwipeDirection}, swipe::{Swipe, SwipeDirection},
theme, theme, ScrollBar,
}, },
}; };
@ -21,7 +21,8 @@ pub struct PassphraseKeyboard {
back: Child<Button<&'static str>>, back: Child<Button<&'static str>>,
confirm: Child<Button<&'static str>>, confirm: Child<Button<&'static str>>,
keys: [[Child<Button<&'static str>>; KEY_COUNT]; PAGE_COUNT], keys: [[Child<Button<&'static str>>; KEY_COUNT]; PAGE_COUNT],
key_page: usize, scrollbar: ScrollBar,
fade: bool,
} }
const STARTING_PAGE: usize = 1; const STARTING_PAGE: usize = 1;
@ -42,11 +43,12 @@ impl PassphraseKeyboard {
Self { Self {
page_swipe: Swipe::horizontal(), page_swipe: Swipe::horizontal(),
input: Input::new().into_child(), input: Input::new().into_child(),
confirm: Button::with_text("Confirm") confirm: Button::with_icon(theme::ICON_CONFIRM)
.styled(theme::button_confirm()) .styled(theme::button_confirm())
.into_child(), .into_child(),
back: Button::with_text("Back") back: Button::with_icon(theme::ICON_BACK)
.styled(theme::button_clear()) .styled(theme::button_reset())
.initially_enabled(false)
.into_child(), .into_child(),
keys: KEYBOARD.map(|page| { keys: KEYBOARD.map(|page| {
page.map(|text| { 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) { fn on_page_swipe(&mut self, ctx: &mut EventCtx, swipe: SwipeDirection) {
// Change the page number. // Change the page number.
self.key_page = match swipe { let key_page = self.scrollbar.active_page;
SwipeDirection::Left => (self.key_page as isize + 1) as usize % PAGE_COUNT, let key_page = match swipe {
SwipeDirection::Right => (self.key_page as isize - 1) as usize % PAGE_COUNT, SwipeDirection::Left => (key_page as isize + 1) as usize % PAGE_COUNT,
_ => self.key_page, SwipeDirection::Right => (key_page as isize - 1) as usize % PAGE_COUNT,
_ => key_page,
}; };
self.scrollbar.go_to(key_page);
// Clear the pending state. // Clear the pending state.
self.input self.input
.mutate(ctx, |ctx, i| i.multi_tap.clear_pending_state(ctx)); .mutate(ctx, |ctx, i| i.multi_tap.clear_pending_state(ctx));
// Make sure to completely repaint the buttons. // 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); btn.request_complete_repaint(ctx);
} }
// Reset backlight to normal level on next paint.
self.fade = true;
} }
fn after_edit(&mut self, ctx: &mut EventCtx) { fn after_edit(&mut self, ctx: &mut EventCtx) {
@ -99,16 +106,28 @@ impl Component for PassphraseKeyboard {
type Msg = PassphraseKeyboardMsg; type Msg = PassphraseKeyboardMsg;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let input_area = Grid::new(bounds, 5, 1).row_col(0, 0); let bounds = bounds.inset(theme::borders());
let confirm_btn_area = Grid::new(bounds, 5, 3).cell(14);
let back_btn_area = Grid::new(bounds, 5, 3).cell(12); let input_area = Grid::new(bounds, 5, 1)
let key_grid = Grid::new(bounds, 5, 3); .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.page_swipe.place(bounds);
self.input.place(input_area); self.input.place(input_area);
self.confirm.place(confirm_btn_area); self.confirm.place(confirm_btn_area);
self.back.place(back_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 // Assign the keys in each page to buttons on a 5x3 grid, starting from the
// second row. // second row.
let area = key_grid.cell(if key < 9 { let area = key_grid.cell(if key < 9 {
@ -152,7 +171,7 @@ impl Component for PassphraseKeyboard {
None 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) { if let Some(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.
@ -170,11 +189,27 @@ impl Component for PassphraseKeyboard {
fn paint(&mut self) { fn paint(&mut self) {
self.input.paint(); self.input.paint();
self.scrollbar.paint();
self.confirm.paint(); self.confirm.paint();
self.back.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(); 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) { fn paint(&mut self) {
let style = theme::label_default(); 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( display::text(
self.area.bottom_left(), text_baseline,
self.textbox.content().as_bytes(), text,
style.font, style.font,
style.text_color, style.text_color,
style.background_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();
} }
} }

View File

@ -42,7 +42,6 @@ impl<T> PinKeyboard<T>
where where
T: Deref<Target = [u8]>, T: Deref<Target = [u8]>,
{ {
const BUTTON_SPACING: i32 = 8;
const HEADER_HEIGHT: i32 = 25; const HEADER_HEIGHT: i32 = 25;
const HEADER_PADDING_SIDE: i32 = 5; const HEADER_PADDING_SIDE: i32 = 5;
const HEADER_PADDING_BOTTOM: i32 = 12; const HEADER_PADDING_BOTTOM: i32 = 12;
@ -57,7 +56,6 @@ where
major_warning: Option<T>, major_warning: Option<T>,
allow_cancel: bool, allow_cancel: bool,
) -> Self { ) -> Self {
let area = area.inset(Insets::right(theme::CONTENT_BORDER));
let digits = Vec::new(); let digits = Vec::new();
// Control buttons. // Control buttons.

View File

@ -11,7 +11,7 @@ use crate::{
model_tt::{ model_tt::{
component::{ component::{
keyboard::{ keyboard::{
common::{MultiTapKeyboard, TextBox, TextEdit}, common::{paint_pending_marker, MultiTapKeyboard, TextBox, TextEdit},
mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT}, mnemonic::{MnemonicInput, MnemonicInputMsg, MNEMONIC_KEY_COUNT},
}, },
Button, ButtonContent, ButtonMsg, Button, ButtonContent, ButtonMsg,
@ -36,7 +36,7 @@ impl MnemonicInput for Slip39Input {
/// Return the key set. Keys are further specified as indices into this /// Return the key set. Keys are further specified as indices into this
/// array. /// array.
fn keys() -> [&'static str; MNEMONIC_KEY_COUNT] { 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 /// 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 // Ignore the pending char rotation. We use the pending key to paint
// the last character, but the mnemonic word computation depends // the last character, but the mnemonic word computation depends
// only on the pressed key, not on the specific character inside it. // 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); self.complete_word_from_dictionary(ctx);
} }
@ -147,20 +149,10 @@ impl Component for Slip39Input {
style.text_color, style.text_color,
style.button_color, style.button_color,
); );
let width = style.font.text_width(text.as_bytes());
// Paint the pending marker. // Paint the pending marker.
if self.multi_tap.pending_key().is_some() && self.final_word.is_none() { if self.multi_tap.pending_key().is_some() && self.final_word.is_none() {
// Measure the width of the last character of input. paint_pending_marker(text_baseline, text.as_bytes(), style.font, style.text_color);
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 the icon. // Paint the icon.
@ -171,6 +163,10 @@ impl Component for Slip39Input {
display::icon(icon_center, icon, style.text_color, style.button_color); 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 { impl Slip39Input {
@ -200,11 +196,15 @@ impl Slip39Input {
fn complete_word_from_dictionary(&mut self, ctx: &mut EventCtx) { fn complete_word_from_dictionary(&mut self, ctx: &mut EventCtx) {
let sequence = self.input_sequence(); let sequence = self.input_sequence();
self.final_word = sequence.and_then(slip39::button_sequence_to_word);
self.input_mask = sequence self.input_mask = sequence
.and_then(slip39::word_completion_mask) .and_then(slip39::word_completion_mask)
.map(Slip39Mask) .map(Slip39Mask)
.unwrap_or_else(Slip39Mask::full); .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. // Change the style of the button depending on the input.
if self.final_word.is_some() { if self.final_word.is_some() {

View File

@ -5,15 +5,23 @@ mod frame;
mod keyboard; mod keyboard;
mod loader; mod loader;
mod page; mod page;
mod scroll;
mod swipe; mod swipe;
pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet}; pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet};
pub use confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use dialog::{Dialog, DialogLayout, DialogMsg}; pub use dialog::{Dialog, DialogLayout, DialogMsg};
pub use frame::Frame; 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 loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use page::SwipePage; pub use page::SwipePage;
pub use scroll::ScrollBar;
pub use swipe::{Swipe, SwipeDirection}; pub use swipe::{Swipe, SwipeDirection};
use super::{event, theme}; use super::{event, theme};

View File

@ -1,12 +1,12 @@
use crate::ui::{ use crate::ui::{
component::{ component::{
base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, Never, Pad, Paginate, base::ComponentExt, paginated::PageMsg, Component, Event, EventCtx, Pad, Paginate,
}, },
display::{self, Color}, 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<T, U> { pub struct SwipePage<T, U> {
content: T, content: T,
@ -151,8 +151,8 @@ where
} }
fn bounds(&self, sink: &mut dyn FnMut(Rect)) { fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.scrollbar.area);
sink(self.pad.area); sink(self.pad.area);
self.scrollbar.bounds(sink);
self.content.bounds(sink); self.content.bounds(sink);
if !self.scrollbar.has_next_page() { if !self.scrollbar.has_next_page() {
self.buttons.bounds(sink); 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<Self::Msg> {
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 struct PageLayout {
pub content_single_page: Rect, pub content_single_page: Rect,
pub content: Rect, pub content: Rect,

View File

@ -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<Self::Msg> {
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);
}
}

View File

@ -12,10 +12,11 @@ use crate::{
use super::{ use super::{
component::{ component::{
Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, PinKeyboard, Bip39Input, Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg,
PinKeyboardMsg, SwipePage, MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard, PassphraseKeyboardMsg,
PinKeyboard, PinKeyboardMsg, Slip39Input, SwipePage,
}, },
constant, theme, theme,
}; };
impl<T> TryFrom<DialogMsg<T, ButtonMsg, ButtonMsg>> for Obj impl<T> TryFrom<DialogMsg<T, ButtonMsg, ButtonMsg>> for Obj
@ -62,6 +63,27 @@ impl TryFrom<PinKeyboardMsg> for Obj {
} }
} }
impl TryFrom<MnemonicKeyboardMsg> for Obj {
type Error = Error;
fn try_from(val: MnemonicKeyboardMsg) -> Result<Self, Self::Error> {
match val {
MnemonicKeyboardMsg::Confirmed => Ok(Obj::const_true()),
}
}
}
impl TryFrom<PassphraseKeyboardMsg> for Obj {
type Error = Error;
fn try_from(val: PassphraseKeyboardMsg) -> Result<Self, Self::Error> {
match val {
PassphraseKeyboardMsg::Confirmed => Ok(Obj::const_true()),
PassphraseKeyboardMsg::Cancelled => Ok(Obj::const_none()),
}
}
}
#[no_mangle] #[no_mangle]
extern "C" fn ui_layout_new_example(_param: Obj) -> Obj { extern "C" fn ui_layout_new_example(_param: Obj) -> Obj {
let block = move || { 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) } 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)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -4,9 +4,7 @@ use crate::ui::{
geometry::Insets, geometry::Insets,
}; };
use super::{ use super::component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet};
component::{ButtonStyle, ButtonStyleSheet, LoaderStyle, LoaderStyleSheet},
};
// Font constants. // Font constants.
pub const FONT_NORMAL: Font = Font::new(-1); pub const FONT_NORMAL: Font = Font::new(-1);
@ -101,8 +99,8 @@ pub fn button_default() -> ButtonStyleSheet {
}, },
active: &ButtonStyle { active: &ButtonStyle {
font: FONT_BOLD, font: FONT_BOLD,
text_color: BG, text_color: FG,
button_color: FG, button_color: GREY_MEDIUM,
background_color: BG, background_color: BG,
border_color: FG, border_color: FG,
border_radius: RADIUS, border_radius: RADIUS,
@ -290,7 +288,7 @@ pub const KEYBOARD_SPACING: i32 = 8;
/// +----------+ /// +----------+
/// | 13 | /// | 13 |
/// | +----+ | /// | +----+ |
/// |10| | 5| /// |10| |10|
/// | +----+ | /// | +----+ |
/// | 14 | /// | 14 |
/// +----------+ /// +----------+

View File

@ -25,12 +25,37 @@ def layout_new_pin(
*, *,
prompt: str, prompt: str,
subprompt: str, subprompt: str,
danger: bool,
allow_cancel: bool, allow_cancel: bool,
warning: str | None,
) -> object: ) -> object:
"""PIN keyboard.""" """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 # extmod/rustmods/modtrezorui2.c
def layout_new_confirm_text( def layout_new_confirm_text(
*, *,

View File

@ -3,9 +3,15 @@ from typing import TYPE_CHECKING
from trezor import io, log, loop, ui, wire, workflow from trezor import io, log, loop, ui, wire, workflow
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezorui2 import layout_new_confirm_action, layout_new_pin from trezorui2 import (
layout_new_bip39,
layout_new_confirm_action,
layout_new_passphrase,
layout_new_pin,
layout_new_slip39,
)
from ...components.tt import pin from ...components.tt import passphrase, pin
from ...constants.tt import MONO_ADDR_PER_LINE from ...constants.tt import MONO_ADDR_PER_LINE
from ..common import button_request, interact 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: 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( async def request_pin_on_device(
@ -475,3 +493,19 @@ async def request_pin_on_device(
raise wire.PinCancelled raise wire.PinCancelled
assert isinstance(result, str) assert isinstance(result, str)
return result 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