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:
parent
efe25a6ab4
commit
4eefaffac9
@ -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)},
|
||||
|
@ -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();
|
||||
|
@ -29,4 +29,5 @@ static void _librust_qstrs(void) {
|
||||
MP_QSTR_subprompt;
|
||||
MP_QSTR_warning;
|
||||
MP_QSTR_allow_cancel;
|
||||
MP_QSTR_max_len;
|
||||
}
|
||||
|
@ -92,4 +92,9 @@ where
|
||||
self.inner.paint();
|
||||
}
|
||||
}
|
||||
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
sink(self.pad.area);
|
||||
self.inner.bounds(sink);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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")]
|
||||
|
@ -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 {
|
||||
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
|
@ -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<Msg = MnemonicInputMsg> {
|
||||
@ -151,3 +165,11 @@ pub enum MnemonicInputMsg {
|
||||
Completed,
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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<Button<&'static str>>,
|
||||
confirm: Child<Button<&'static str>>,
|
||||
keys: [[Child<Button<&'static str>>; 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) {
|
||||
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(
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ impl<T> PinKeyboard<T>
|
||||
where
|
||||
T: Deref<Target = [u8]>,
|
||||
{
|
||||
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<T>,
|
||||
allow_cancel: bool,
|
||||
) -> Self {
|
||||
let area = area.inset(Insets::right(theme::CONTENT_BORDER));
|
||||
let digits = Vec::new();
|
||||
|
||||
// Control buttons.
|
||||
|
@ -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() {
|
||||
|
@ -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};
|
||||
|
@ -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<T, U> {
|
||||
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<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 content_single_page: Rect,
|
||||
pub content: Rect,
|
||||
|
138
core/embed/rust/src/ui/model_tt/component/scroll.rs
Normal file
138
core/embed/rust/src/ui/model_tt/component/scroll.rs
Normal 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);
|
||||
}
|
||||
}
|
@ -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<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]
|
||||
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::{
|
||||
|
@ -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 |
|
||||
/// +----------+
|
||||
|
@ -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(
|
||||
*,
|
||||
|
@ -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 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 ..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
|
||||
|
Loading…
Reference in New Issue
Block a user