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."""
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)},

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_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();

View File

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

View File

@ -92,4 +92,9 @@ where
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)]
pub struct LinearPlacement {
axis: Axis,
align: Alignment,
spacing: i32,
pub axis: Axis,
pub align: Alignment,
pub spacing: i32,
}
impl LinearPlacement {

View File

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

View File

@ -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")]

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -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() {

View File

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

View File

@ -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,

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::{
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::{

View File

@ -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 |
/// +----------+

View File

@ -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(
*,

View File

@ -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