mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-26 09:28:13 +00:00
fix(core/rust/ui): pin keyboard tweaks
[no changelog]
This commit is contained in:
parent
d51072b8c1
commit
efe25a6ab4
@ -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)},
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<T> Component for Label<T>
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<Offset> 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)]
|
||||
|
@ -61,6 +61,13 @@ impl<T> Button<T> {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -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<T> {
|
||||
digits: Vec<u8, MAX_LENGTH>,
|
||||
major_prompt: Label<&'static [u8]>,
|
||||
minor_prompt: Label<&'static [u8]>,
|
||||
allow_cancel: bool,
|
||||
major_prompt: Label<T>,
|
||||
minor_prompt: Label<T>,
|
||||
major_warning: Option<Label<T>>,
|
||||
dots: Child<PinDots>,
|
||||
reset_btn: Child<Button<&'static str>>,
|
||||
cancel_btn: Child<Button<&'static str>>,
|
||||
reset_btn: Child<Maybe<Button<&'static str>>>,
|
||||
cancel_btn: Child<Maybe<Button<&'static str>>>,
|
||||
confirm_btn: Child<Button<&'static str>>,
|
||||
digit_btns: [Child<Button<&'static str>>; DIGIT_COUNT],
|
||||
}
|
||||
|
||||
impl PinKeyboard {
|
||||
pub fn new(major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self {
|
||||
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;
|
||||
|
||||
// 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<T>,
|
||||
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<T> Component for PinKeyboard<T>
|
||||
where
|
||||
T: Deref<Target = [u8]>,
|
||||
{
|
||||
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<T> crate::trace::Trace for PinKeyboard<T>
|
||||
where
|
||||
T: Deref<Target = [u8]>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("PinKeyboard");
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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;
|
||||
|
@ -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<T> TryFrom<DialogMsg<T, ButtonMsg, ButtonMsg>> for Obj
|
||||
@ -52,6 +51,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<PinKeyboardMsg> for Obj {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(val: PinKeyboardMsg) -> Result<Self, Self::Error> {
|
||||
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<bool> =
|
||||
kwargs.get(Qstr::MP_QSTR_allow_cancel)?.try_into_option()?;
|
||||
let warning: Option<Buffer> = 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::{
|
||||
|
BIN
core/embed/rust/src/ui/model_tt/res/back.toif
Normal file
BIN
core/embed/rust/src/ui/model_tt/res/back.toif
Normal file
Binary file not shown.
@ -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 |
|
||||
|
@ -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(
|
||||
*,
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user