diff --git a/core/embed/extmod/rustmods/modtrezorui2.c b/core/embed/extmod/rustmods/modtrezorui2.c index 633209bb3..01ede0212 100644 --- a/core/embed/extmod/rustmods/modtrezorui2.c +++ b/core/embed/extmod/rustmods/modtrezorui2.c @@ -42,6 +42,17 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_confirm_action_obj, /// """Example layout.""" STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj, ui_layout_new_example); + +/// def layout_new_pin( +/// *, +/// prompt: str, +/// subprompt: str, +/// allow_cancel: bool, +/// warning: str | None, +/// ) -> object: +/// """PIN keyboard.""" +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_pin_obj, 0, + ui_layout_new_pin); #elif TREZOR_MODEL == 1 /// def layout_new_confirm_text( /// *, @@ -62,6 +73,8 @@ STATIC const mp_rom_map_elem_t mp_module_trezorui2_globals_table[] = { #if TREZOR_MODEL == T {MP_ROM_QSTR(MP_QSTR_layout_new_example), 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)}, #elif TREZOR_MODEL == 1 {MP_ROM_QSTR(MP_QSTR_layout_new_confirm_text), MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_text_obj)}, diff --git a/core/embed/rust/librust.h b/core/embed/rust/librust.h index a9d9bc2ac..2f4ecbe9a 100644 --- a/core/embed/rust/librust.h +++ b/core/embed/rust/librust.h @@ -17,6 +17,8 @@ mp_obj_t ui_layout_new_confirm_action(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs); 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); #ifdef TREZOR_EMULATOR mp_obj_t ui_debug_layout_type(); diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h index 6847f288a..2a44d1df8 100644 --- a/core/embed/rust/librust_qstr.h +++ b/core/embed/rust/librust_qstr.h @@ -25,4 +25,8 @@ static void _librust_qstrs(void) { MP_QSTR_verb; MP_QSTR_verb_cancel; MP_QSTR_reverse; + MP_QSTR_prompt; + MP_QSTR_subprompt; + MP_QSTR_warning; + MP_QSTR_allow_cancel; } diff --git a/core/embed/rust/src/ui/component/label.rs b/core/embed/rust/src/ui/component/label.rs index f81b645b1..68d31f56e 100644 --- a/core/embed/rust/src/ui/component/label.rs +++ b/core/embed/rust/src/ui/component/label.rs @@ -47,6 +47,13 @@ where pub fn text(&self) -> &T { &self.text } + + pub fn size(&self) -> Offset { + Offset::new( + self.style.font.text_width(&self.text), + self.style.font.text_height(), + ) + } } impl Component for Label @@ -56,31 +63,14 @@ where type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { - let size = Offset::new( - self.style.font.text_width(&self.text), - self.style.font.line_height(), - ); - self.area = match self.align { - Alignment::Start => Rect::from_top_left_and_size(bounds.top_left(), size), - Alignment::Center => { - let origin = bounds.top_left().center(bounds.top_right()); - Rect { - x0: origin.x - size.x / 2, - y0: origin.y, - x1: origin.x + size.x / 2, - y1: origin.y + size.y, - } - } - Alignment::End => { - let origin = bounds.top_right(); - Rect { - x0: origin.x - size.x, - y0: origin.y, - x1: origin.x, - y1: origin.y + size.y, - } - } + let origin = match self.align { + Alignment::Start => bounds.top_left(), + Alignment::Center => bounds.top_left().center(bounds.top_right()), + Alignment::End => bounds.top_right(), }; + let size = self.size(); + let top_left = size.snap(origin, self.align, Alignment::Start); + self.area = Rect::from_top_left_and_size(top_left, size); self.area } @@ -97,4 +87,8 @@ where self.style.background_color, ); } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area) + } } diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index d359cb55b..c884dcf45 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -47,6 +47,22 @@ impl Offset { pub fn abs(self) -> Self { Self::new(self.x.abs(), self.y.abs()) } + + /// With `self` representing a rectangle size, returns top-left corner of + /// the rectangle such that it is aligned relative to the `point`. + pub fn snap(self, point: Point, x: Alignment, y: Alignment) -> Point { + let x_off = match x { + Alignment::Start => 0, + Alignment::Center => self.x / 2, + Alignment::End => self.x, + }; + let y_off = match y { + Alignment::Start => 0, + Alignment::Center => self.y / 2, + Alignment::End => self.y, + }; + point - Self::new(x_off, y_off) + } } impl Add for Offset { @@ -280,6 +296,15 @@ impl Rect { pub fn split_right(self, width: i32) -> (Self, Self) { self.split_left(self.width() - width) } + + pub fn translate(&self, offset: Offset) -> Self { + Self { + x0: self.x0 + offset.x, + y0: self.y0 + offset.y, + x1: self.x1 + offset.x, + y1: self.y1 + offset.y, + } + } } #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index 7de2dee03..af688ef09 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -61,6 +61,13 @@ impl Button { } } + pub fn initially_enabled(mut self, enabled: bool) -> Self { + if !enabled { + self.state = State::Disabled; + } + self + } + pub fn enable(&mut self, ctx: &mut EventCtx) { self.set(ctx, State::Initial) } diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs index 7b5caef4e..56316e59f 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs @@ -1,15 +1,15 @@ +use core::ops::Deref; use heapless::Vec; use crate::{ trezorhal::random, ui::{ component::{ - base::ComponentExt, - label::{Label, LabelStyle}, - Child, Component, Event, EventCtx, Never, + base::ComponentExt, Child, Component, Event, EventCtx, Label, LabelStyle, Maybe, Never, + Pad, }, display, - geometry::{Grid, Offset, Point, Rect}, + geometry::{Alignment, Grid, Insets, Offset, Rect}, model_tt::component::{ button::{Button, ButtonContent, ButtonMsg::Clicked}, theme, @@ -25,35 +25,66 @@ pub enum PinKeyboardMsg { const MAX_LENGTH: usize = 9; const DIGIT_COUNT: usize = 10; // 0..10 -pub struct PinKeyboard { +pub struct PinKeyboard { digits: Vec, - major_prompt: Label<&'static [u8]>, - minor_prompt: Label<&'static [u8]>, + allow_cancel: bool, + major_prompt: Label, + minor_prompt: Label, + major_warning: Option>, dots: Child, - reset_btn: Child>, - cancel_btn: Child>, + reset_btn: Child>>, + cancel_btn: Child>>, confirm_btn: Child>, digit_btns: [Child>; DIGIT_COUNT], } -impl PinKeyboard { - pub fn new(major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self { +impl PinKeyboard +where + T: Deref, +{ + const BUTTON_SPACING: i32 = 8; + const HEADER_HEIGHT: i32 = 25; + const HEADER_PADDING_SIDE: i32 = 5; + const HEADER_PADDING_BOTTOM: i32 = 12; + + // Label position fine-tuning. + const MAJOR_OFF: Offset = Offset::y(-2); + const MINOR_OFF: Offset = Offset::y(-1); + + pub fn new( + major_prompt: T, + minor_prompt: T, + major_warning: Option, + allow_cancel: bool, + ) -> Self { + let area = area.inset(Insets::right(theme::CONTENT_BORDER)); let digits = Vec::new(); + + // Control buttons. + let reset_btn = Button::with_icon(theme::ICON_BACK) + .styled(theme::button_reset()) + .initially_enabled(false); + let reset_btn = Maybe::hidden(theme::BG, reset_btn).into_child(); + + let cancel_btn = Button::with_icon(theme::ICON_CANCEL).styled(theme::button_cancel()); + let cancel_btn = + Maybe::new(Pad::with_background(theme::BG), cancel_btn, allow_cancel).into_child(); + Self { - major_prompt: Label::centered(major_prompt, theme::label_default()), - minor_prompt: Label::centered(minor_prompt, theme::label_default()), - dots: PinDots::new(digits.len(), theme::label_default()).into_child(), - reset_btn: Button::with_text("Reset") - .styled(theme::button_clear()) - .into_child(), - cancel_btn: Button::with_icon(theme::ICON_CANCEL) - .styled(theme::button_cancel()) - .into_child(), + digits, + allow_cancel, + major_prompt: Label::left_aligned(major_prompt, theme::label_keyboard()), + minor_prompt: Label::right_aligned(minor_prompt, theme::label_keyboard_minor()), + major_warning: major_warning + .map(|text| Label::left_aligned(text, theme::label_keyboard_warning())), + dots: PinDots::new(0, theme::label_default()).into_child(), + reset_btn, + cancel_btn, confirm_btn: Button::with_icon(theme::ICON_CONFIRM) - .styled(theme::button_clear()) + .styled(theme::button_confirm()) + .initially_enabled(false) .into_child(), digit_btns: Self::generate_digit_buttons(), - digits, } } @@ -61,7 +92,10 @@ impl PinKeyboard { // Generate a random sequence of digits from 0 to 9. let mut digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; random::shuffle(&mut digits); - digits.map(Button::with_text).map(Child::new) + digits + .map(Button::with_text) + .map(|b| b.styled(theme::button_pin())) + .map(Child::new) } fn pin_modified(&mut self, ctx: &mut EventCtx) { @@ -70,10 +104,15 @@ impl PinKeyboard { btn.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_full)); } let is_empty = self.digits.is_empty(); - self.reset_btn - .mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty)); - self.cancel_btn - .mutate(ctx, |ctx, btn| btn.enable_if(ctx, is_empty)); + let cancel_enabled = is_empty && self.allow_cancel; + self.reset_btn.mutate(ctx, |ctx, btn| { + btn.show_if(ctx, !is_empty); + btn.inner_mut().enable_if(ctx, !is_empty); + }); + self.cancel_btn.mutate(ctx, |ctx, btn| { + btn.show_if(ctx, cancel_enabled); + btn.inner_mut().enable_if(ctx, is_empty); + }); self.confirm_btn .mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty)); let digit_count = self.digits.len(); @@ -86,36 +125,49 @@ impl PinKeyboard { } } -impl Component for PinKeyboard { +impl Component for PinKeyboard +where + T: Deref, +{ type Msg = PinKeyboardMsg; fn place(&mut self, bounds: Rect) -> Rect { // Prompts and PIN dots display. - let grid = if self.minor_prompt.text().is_empty() { - // Make the major prompt bigger if the minor one is empty. - Grid::new(bounds, 5, 1) - } else { - Grid::new(bounds, 6, 1) - }; - self.major_prompt.place(grid.row_col(0, 0)); - self.minor_prompt.place(grid.row_col(0, 1)); - self.dots.place(grid.row_col(0, 0)); + let (header, keypad) = bounds + .inset(theme::borders()) + .split_top(Self::HEADER_HEIGHT + Self::HEADER_PADDING_BOTTOM); + let header = header.inset(Insets::new( + 0, + Self::HEADER_PADDING_SIDE, + Self::HEADER_PADDING_BOTTOM, + Self::HEADER_PADDING_SIDE, + )); + let major_area = header.translate(Self::MAJOR_OFF); + let minor_area = header.translate(Self::MINOR_OFF); // Control buttons. - let grid = Grid::new(bounds, 5, 3); - self.reset_btn.place(grid.row_col(4, 0)); - self.cancel_btn.place(grid.row_col(4, 0)); - self.confirm_btn.place(grid.row_col(4, 2)); + let grid = Grid::new(keypad, 4, 3).with_spacing(theme::KEYBOARD_SPACING); + + // Prompts and PIN dots display. + self.dots.place(header); + self.major_prompt.place(major_area); + self.minor_prompt.place(minor_area); + self.major_warning.as_mut().map(|c| c.place(major_area)); + + // Control buttons. + let reset_cancel_area = grid.row_col(3, 0); + self.reset_btn.place(reset_cancel_area); + self.cancel_btn.place(reset_cancel_area); + self.confirm_btn.place(grid.row_col(3, 2)); // Digit buttons. for (i, btn) in self.digit_btns.iter_mut().enumerate() { - // Assign the digits to buttons on a 5x3 grid, starting from the second row. + // Assign the digits to buttons on a 4x3 grid, starting from the first row. let area = grid.cell(if i < 9 { - // The grid has 3 columns, and we skip the first row. - i + 3 + i } else { // For the last key (the "0" position) we skip one cell. - i + 1 + 3 + i + 1 }); btn.place(area); } @@ -151,12 +203,17 @@ impl Component for PinKeyboard { } fn paint(&mut self) { + self.reset_btn.paint(); if self.digits.is_empty() { - self.cancel_btn.paint(); - self.major_prompt.paint(); + self.dots.inner().clear(); + if let Some(ref mut w) = self.major_warning { + w.paint(); + } else { + self.major_prompt.paint(); + } self.minor_prompt.paint(); + self.cancel_btn.paint(); } else { - self.reset_btn.paint(); self.dots.paint(); } self.confirm_btn.paint(); @@ -164,6 +221,18 @@ impl Component for PinKeyboard { btn.paint(); } } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + self.major_prompt.bounds(sink); + self.minor_prompt.bounds(sink); + self.reset_btn.bounds(sink); + self.cancel_btn.bounds(sink); + self.confirm_btn.bounds(sink); + self.dots.bounds(sink); + for b in &self.digit_btns { + b.bounds(sink) + } + } } struct PinDots { @@ -173,7 +242,7 @@ struct PinDots { } impl PinDots { - const DOT: i32 = 10; + const DOT: i32 = 6; const PADDING: i32 = 4; fn new(digit_count: usize, style: LabelStyle) -> Self { @@ -190,6 +259,17 @@ impl PinDots { ctx.request_paint(); } } + + /// Clear the area with the background color. + fn clear(&self) { + display::rect_fill(self.area, self.style.background_color); + } + + fn size(&self) -> Offset { + let mut width = Self::DOT * (self.digit_count as i32); + width += Self::PADDING * (self.digit_count.saturating_sub(1) as i32); + Offset::new(width, Self::DOT) + } } impl Component for PinDots { @@ -205,22 +285,36 @@ impl Component for PinDots { } fn paint(&mut self) { - // Clear the area with the background color. - display::rect_fill(self.area, self.style.background_color); + self.clear(); + + let mut cursor = self + .size() + .snap(self.area.center(), Alignment::Center, Alignment::Center); // Draw a dot for each PIN digit. - for i in 0..self.digit_count { - let pos = Point { - x: self.area.x0 + i as i32 * (Self::DOT + Self::PADDING), - y: self.area.center().y, - }; - let size = Offset::new(Self::DOT, Self::DOT); - display::rect_fill_rounded( - Rect::from_top_left_and_size(pos, size), + for _ in 0..self.digit_count { + display::icon_top_left( + cursor, + theme::DOT_ACTIVE, self.style.text_color, self.style.background_color, - 4, ); + cursor.x += Self::DOT + Self::PADDING; } } + + fn bounds(&self, sink: &mut dyn FnMut(Rect)) { + sink(self.area); + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for PinKeyboard +where + T: Deref, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.open("PinKeyboard"); + t.close(); + } } diff --git a/core/embed/rust/src/ui/model_tt/component/mod.rs b/core/embed/rust/src/ui/model_tt/component/mod.rs index 47ae587f0..60ba2b670 100644 --- a/core/embed/rust/src/ui/model_tt/component/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/mod.rs @@ -11,6 +11,7 @@ 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 loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use page::SwipePage; pub use swipe::{Swipe, SwipeDirection}; diff --git a/core/embed/rust/src/ui/model_tt/component/page.rs b/core/embed/rust/src/ui/model_tt/component/page.rs index 348574532..6e8512880 100644 --- a/core/embed/rust/src/ui/model_tt/component/page.rs +++ b/core/embed/rust/src/ui/model_tt/component/page.rs @@ -189,8 +189,6 @@ impl ScrollBar { /// Edge of last dot to center of arrow icon. const ARROW_SPACE: i32 = 26; - const ICON_ACTIVE: &'static [u8] = include_res!("model_tt/res/scroll-active.toif"); - const ICON_INACTIVE: &'static [u8] = include_res!("model_tt/res/scroll-inactive.toif"); 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"); @@ -253,9 +251,9 @@ impl Component for ScrollBar { let mut top = None; let mut display_icon = |top_left| { let icon = if i == self.active_page { - Self::ICON_ACTIVE + theme::DOT_ACTIVE } else { - Self::ICON_INACTIVE + theme::DOT_INACTIVE }; display::icon_top_left(top_left, icon, theme::FG, theme::BG); i += 1; diff --git a/core/embed/rust/src/ui/model_tt/layout.rs b/core/embed/rust/src/ui/model_tt/layout.rs index 300341865..7a54ab49a 100644 --- a/core/embed/rust/src/ui/model_tt/layout.rs +++ b/core/embed/rust/src/ui/model_tt/layout.rs @@ -12,11 +12,10 @@ use crate::{ use super::{ component::{ - Bip39Input, Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, - MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard, PassphraseKeyboardMsg, - PinKeyboard, PinKeyboardMsg, Slip39Input, SwipePage, + Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, PinKeyboard, + PinKeyboardMsg, SwipePage, }, - theme, + constant, theme, }; impl TryFrom> for Obj @@ -52,6 +51,17 @@ where } } +impl TryFrom for Obj { + type Error = Error; + + fn try_from(val: PinKeyboardMsg) -> Result { + match val { + PinKeyboardMsg::Confirmed => 1.try_into(), + PinKeyboardMsg::Cancelled => 2.try_into(), + } + } +} + #[no_mangle] extern "C" fn ui_layout_new_example(_param: Obj) -> Obj { let block = move || { @@ -112,6 +122,22 @@ extern "C" fn ui_layout_new_confirm_action( unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } } +#[no_mangle] +extern "C" fn ui_layout_new_pin(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 subprompt: Buffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?; + let allow_cancel: Option = + kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into_option()?; + let warning: Option = kwargs.get(Qstr::MP_QSTR_warning)?.try_into_option()?; + let obj = LayoutObj::new( + PinKeyboard::new(prompt, subprompt, warning, allow_cancel.unwrap_or(true)).into_child(), + )?; + Ok(obj.into()) + }; + unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/core/embed/rust/src/ui/model_tt/res/back.toif b/core/embed/rust/src/ui/model_tt/res/back.toif new file mode 100644 index 000000000..7c5824a97 Binary files /dev/null and b/core/embed/rust/src/ui/model_tt/res/back.toif differ diff --git a/core/embed/rust/src/ui/model_tt/theme.rs b/core/embed/rust/src/ui/model_tt/theme.rs index ca441a29a..e375f4167 100644 --- a/core/embed/rust/src/ui/model_tt/theme.rs +++ b/core/embed/rust/src/ui/model_tt/theme.rs @@ -27,11 +27,16 @@ pub const BLACK: Color = Color::rgb(0, 0, 0); pub const FG: Color = WHITE; // Default foreground (text & icon) color. pub const BG: Color = BLACK; // Default background color. pub const RED: Color = Color::rgb(205, 73, 73); // dark-coral +pub const RED_DARK: Color = Color::rgb(166, 45, 45); pub const YELLOW: Color = Color::rgb(193, 144, 9); // ochre +pub const YELLOW_DARK: Color = Color::rgb(154, 115, 6); // FIXME pub const GREEN: Color = Color::rgb(57, 168, 20); // grass-green +pub const GREEN_DARK: Color = Color::rgb(48, 147, 15); pub const BLUE: Color = Color::rgb(0, 86, 190); // blue +pub const OFF_WHITE: Color = Color::rgb(222, 222, 222); // very light grey pub const GREY_LIGHT: Color = Color::rgb(168, 168, 168); // greyish -pub const GREY_DARK: Color = Color::rgb(51, 51, 51); // black +pub const GREY_MEDIUM: Color = Color::rgb(100, 100, 100); +pub const GREY_DARK: Color = Color::rgb(51, 51, 51); // greyer // Commonly used corner radius (i.e. for buttons). pub const RADIUS: u8 = 2; @@ -43,8 +48,13 @@ pub const ICON_SIZE: i32 = 16; pub const ICON_CANCEL: &[u8] = include_res!("model_tt/res/cancel.toif"); pub const ICON_CONFIRM: &[u8] = include_res!("model_tt/res/confirm.toif"); pub const ICON_SPACE: &[u8] = include_res!("model_tt/res/space.toif"); -pub const ICON_BACK: &[u8] = include_res!("model_tt/res/left.toif"); +pub const ICON_BACK: &[u8] = include_res!("model_tt/res/back.toif"); pub const ICON_CLICK: &[u8] = include_res!("model_tt/res/click.toif"); +pub const ICON_NEXT: &[u8] = include_res!("model_tt/res/next.toif"); + +// Scrollbar/PIN dots. +pub const DOT_ACTIVE: &[u8] = include_res!("model_tt/res/scroll-active.toif"); +pub const DOT_INACTIVE: &[u8] = include_res!("model_tt/res/scroll-inactive.toif"); pub fn label_default() -> LabelStyle { LabelStyle { @@ -54,6 +64,30 @@ pub fn label_default() -> LabelStyle { } } +pub fn label_keyboard() -> LabelStyle { + LabelStyle { + font: FONT_MEDIUM, + text_color: OFF_WHITE, + background_color: BG, + } +} + +pub fn label_keyboard_warning() -> LabelStyle { + LabelStyle { + font: FONT_MEDIUM, + text_color: RED, + background_color: BG, + } +} + +pub fn label_keyboard_minor() -> LabelStyle { + LabelStyle { + font: FONT_NORMAL, + text_color: OFF_WHITE, + background_color: BG, + } +} + pub fn button_default() -> ButtonStyleSheet { ButtonStyleSheet { normal: &ButtonStyle { @@ -99,8 +133,8 @@ pub fn button_confirm() -> ButtonStyleSheet { }, active: &ButtonStyle { font: FONT_BOLD, - text_color: BG, - button_color: FG, + text_color: FG, + button_color: GREEN_DARK, background_color: BG, border_color: FG, border_radius: RADIUS, @@ -108,7 +142,7 @@ pub fn button_confirm() -> ButtonStyleSheet { }, disabled: &ButtonStyle { font: FONT_BOLD, - text_color: GREY_LIGHT, + text_color: FG, button_color: GREEN, background_color: BG, border_color: BG, @@ -119,7 +153,99 @@ pub fn button_confirm() -> ButtonStyleSheet { } pub fn button_cancel() -> ButtonStyleSheet { - button_default() + ButtonStyleSheet { + normal: &ButtonStyle { + font: FONT_BOLD, + text_color: FG, + button_color: RED, + background_color: BG, + border_color: BG, + border_radius: RADIUS, + border_width: 0, + }, + active: &ButtonStyle { + font: FONT_BOLD, + text_color: FG, + button_color: RED_DARK, + background_color: BG, + border_color: FG, + border_radius: RADIUS, + border_width: 0, + }, + disabled: &ButtonStyle { + font: FONT_BOLD, + text_color: GREY_LIGHT, + button_color: RED, + background_color: BG, + border_color: BG, + border_radius: RADIUS, + border_width: 0, + }, + } +} + +pub fn button_reset() -> ButtonStyleSheet { + ButtonStyleSheet { + normal: &ButtonStyle { + font: FONT_BOLD, + text_color: FG, + button_color: YELLOW, + background_color: BG, + border_color: BG, + border_radius: RADIUS, + border_width: 0, + }, + active: &ButtonStyle { + font: FONT_BOLD, + text_color: FG, + button_color: YELLOW_DARK, + background_color: BG, + border_color: FG, + border_radius: RADIUS, + border_width: 0, + }, + disabled: &ButtonStyle { + font: FONT_BOLD, + text_color: GREY_LIGHT, + button_color: YELLOW, + background_color: BG, + border_color: BG, + border_radius: RADIUS, + border_width: 0, + }, + } +} + +pub fn button_pin() -> ButtonStyleSheet { + ButtonStyleSheet { + normal: &ButtonStyle { + font: FONT_MONO, + text_color: FG, + button_color: GREY_DARK, + background_color: BG, + border_color: BG, + border_radius: RADIUS, + border_width: 0, + }, + active: &ButtonStyle { + font: FONT_MONO, + text_color: FG, + button_color: GREY_MEDIUM, + background_color: BG, + border_color: FG, + border_radius: RADIUS, + border_width: 0, + }, + disabled: &ButtonStyle { + font: FONT_MONO, + text_color: GREY_LIGHT, + button_color: GREY_DARK, + background_color: BG, + border_color: BG, + border_radius: RADIUS, + border_width: 0, + }, + } } pub fn button_clear() -> ButtonStyleSheet { @@ -159,6 +285,7 @@ impl DefaultTextTheme for TTDefaultText { } pub const CONTENT_BORDER: i32 = 5; +pub const KEYBOARD_SPACING: i32 = 8; /// +----------+ /// | 13 | diff --git a/core/mocks/generated/trezorui2.pyi b/core/mocks/generated/trezorui2.pyi index e8b360c06..9b26d9706 100644 --- a/core/mocks/generated/trezorui2.pyi +++ b/core/mocks/generated/trezorui2.pyi @@ -20,6 +20,17 @@ def layout_new_example(text: str) -> object: """Example layout.""" +# extmod/rustmods/modtrezorui2.c +def layout_new_pin( + *, + prompt: str, + subprompt: str, + danger: bool, + allow_cancel: bool, +) -> object: + """PIN keyboard.""" + + # extmod/rustmods/modtrezorui2.c def layout_new_confirm_text( *, diff --git a/core/src/trezor/ui/layouts/tt_v2/__init__.py b/core/src/trezor/ui/layouts/tt_v2/__init__.py index 63ea8d8c4..7edfc404d 100644 --- a/core/src/trezor/ui/layouts/tt_v2/__init__.py +++ b/core/src/trezor/ui/layouts/tt_v2/__init__.py @@ -3,10 +3,11 @@ 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 +from trezorui2 import layout_new_confirm_action, layout_new_pin +from ...components.tt import pin from ...constants.tt import MONO_ADDR_PER_LINE -from ..common import interact +from ..common import button_request, interact if TYPE_CHECKING: from typing import Any, Awaitable, Iterable, NoReturn, Sequence @@ -451,4 +452,26 @@ async def request_pin_on_device( attempts_remaining: int | None, allow_cancel: bool, ) -> str: - raise NotImplementedError + await button_request(ctx, "pin_device", code=ButtonRequestType.PinEntry) + + if attempts_remaining is None: + subprompt = "" + elif attempts_remaining == 1: + subprompt = "Last attempt" + else: + subprompt = f"{attempts_remaining} tries left" + + dialog = _RustLayout( + layout_new_pin( + prompt=prompt, + subprompt=subprompt, + allow_cancel=allow_cancel, + warning=None, + ) + ) + while True: + result = await ctx.wait(dialog) + if result is pin.CANCELLED: + raise wire.PinCancelled + assert isinstance(result, str) + return result