1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-18 10:32:02 +00:00

fix(core/rust/ui): pin keyboard tweaks

[no changelog]
This commit is contained in:
Martin Milata 2022-02-07 17:09:39 +01:00
parent d51072b8c1
commit efe25a6ab4
14 changed files with 426 additions and 101 deletions

View File

@ -42,6 +42,17 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_trezorui2_layout_new_confirm_action_obj,
/// """Example layout.""" /// """Example layout."""
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj, STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorui2_layout_new_example_obj,
ui_layout_new_example); 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 #elif TREZOR_MODEL == 1
/// def layout_new_confirm_text( /// 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 #if TREZOR_MODEL == T
{MP_ROM_QSTR(MP_QSTR_layout_new_example), {MP_ROM_QSTR(MP_QSTR_layout_new_example),
MP_ROM_PTR(&mod_trezorui2_layout_new_example_obj)}, MP_ROM_PTR(&mod_trezorui2_layout_new_example_obj)},
{MP_ROM_QSTR(MP_QSTR_layout_new_pin),
MP_ROM_PTR(&mod_trezorui2_layout_new_pin_obj)},
#elif TREZOR_MODEL == 1 #elif TREZOR_MODEL == 1
{MP_ROM_QSTR(MP_QSTR_layout_new_confirm_text), {MP_ROM_QSTR(MP_QSTR_layout_new_confirm_text),
MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_text_obj)}, MP_ROM_PTR(&mod_trezorui2_layout_new_confirm_text_obj)},

View File

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

View File

@ -25,4 +25,8 @@ static void _librust_qstrs(void) {
MP_QSTR_verb; MP_QSTR_verb;
MP_QSTR_verb_cancel; MP_QSTR_verb_cancel;
MP_QSTR_reverse; MP_QSTR_reverse;
MP_QSTR_prompt;
MP_QSTR_subprompt;
MP_QSTR_warning;
MP_QSTR_allow_cancel;
} }

View File

@ -47,6 +47,13 @@ where
pub fn text(&self) -> &T { pub fn text(&self) -> &T {
&self.text &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> impl<T> Component for Label<T>
@ -56,31 +63,14 @@ where
type Msg = Never; type Msg = Never;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
let size = Offset::new( let origin = match self.align {
self.style.font.text_width(&self.text), Alignment::Start => bounds.top_left(),
self.style.font.line_height(), Alignment::Center => bounds.top_left().center(bounds.top_right()),
); Alignment::End => bounds.top_right(),
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 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 self.area
} }
@ -97,4 +87,8 @@ where
self.style.background_color, self.style.background_color,
); );
} }
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area)
}
} }

View File

@ -47,6 +47,22 @@ impl Offset {
pub fn abs(self) -> Self { pub fn abs(self) -> Self {
Self::new(self.x.abs(), self.y.abs()) 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 { impl Add<Offset> for Offset {
@ -280,6 +296,15 @@ impl Rect {
pub fn split_right(self, width: i32) -> (Self, Self) { pub fn split_right(self, width: i32) -> (Self, Self) {
self.split_left(self.width() - width) 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)] #[derive(Copy, Clone, PartialEq, Eq)]

View File

@ -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) { pub fn enable(&mut self, ctx: &mut EventCtx) {
self.set(ctx, State::Initial) self.set(ctx, State::Initial)
} }

View File

@ -1,15 +1,15 @@
use core::ops::Deref;
use heapless::Vec; use heapless::Vec;
use crate::{ use crate::{
trezorhal::random, trezorhal::random,
ui::{ ui::{
component::{ component::{
base::ComponentExt, base::ComponentExt, Child, Component, Event, EventCtx, Label, LabelStyle, Maybe, Never,
label::{Label, LabelStyle}, Pad,
Child, Component, Event, EventCtx, Never,
}, },
display, display,
geometry::{Grid, Offset, Point, Rect}, geometry::{Alignment, Grid, Insets, Offset, Rect},
model_tt::component::{ model_tt::component::{
button::{Button, ButtonContent, ButtonMsg::Clicked}, button::{Button, ButtonContent, ButtonMsg::Clicked},
theme, theme,
@ -25,35 +25,66 @@ pub enum PinKeyboardMsg {
const MAX_LENGTH: usize = 9; const MAX_LENGTH: usize = 9;
const DIGIT_COUNT: usize = 10; // 0..10 const DIGIT_COUNT: usize = 10; // 0..10
pub struct PinKeyboard { pub struct PinKeyboard<T> {
digits: Vec<u8, MAX_LENGTH>, digits: Vec<u8, MAX_LENGTH>,
major_prompt: Label<&'static [u8]>, allow_cancel: bool,
minor_prompt: Label<&'static [u8]>, major_prompt: Label<T>,
minor_prompt: Label<T>,
major_warning: Option<Label<T>>,
dots: Child<PinDots>, dots: Child<PinDots>,
reset_btn: Child<Button<&'static str>>, reset_btn: Child<Maybe<Button<&'static str>>>,
cancel_btn: Child<Button<&'static str>>, cancel_btn: Child<Maybe<Button<&'static str>>>,
confirm_btn: Child<Button<&'static str>>, confirm_btn: Child<Button<&'static str>>,
digit_btns: [Child<Button<&'static str>>; DIGIT_COUNT], digit_btns: [Child<Button<&'static str>>; DIGIT_COUNT],
} }
impl PinKeyboard { impl<T> PinKeyboard<T>
pub fn new(major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self { 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(); 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 { Self {
major_prompt: Label::centered(major_prompt, theme::label_default()), digits,
minor_prompt: Label::centered(minor_prompt, theme::label_default()), allow_cancel,
dots: PinDots::new(digits.len(), theme::label_default()).into_child(), major_prompt: Label::left_aligned(major_prompt, theme::label_keyboard()),
reset_btn: Button::with_text("Reset") minor_prompt: Label::right_aligned(minor_prompt, theme::label_keyboard_minor()),
.styled(theme::button_clear()) major_warning: major_warning
.into_child(), .map(|text| Label::left_aligned(text, theme::label_keyboard_warning())),
cancel_btn: Button::with_icon(theme::ICON_CANCEL) dots: PinDots::new(0, theme::label_default()).into_child(),
.styled(theme::button_cancel()) reset_btn,
.into_child(), cancel_btn,
confirm_btn: Button::with_icon(theme::ICON_CONFIRM) confirm_btn: Button::with_icon(theme::ICON_CONFIRM)
.styled(theme::button_clear()) .styled(theme::button_confirm())
.initially_enabled(false)
.into_child(), .into_child(),
digit_btns: Self::generate_digit_buttons(), digit_btns: Self::generate_digit_buttons(),
digits,
} }
} }
@ -61,7 +92,10 @@ impl PinKeyboard {
// Generate a random sequence of digits from 0 to 9. // Generate a random sequence of digits from 0 to 9.
let mut digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; let mut digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
random::shuffle(&mut digits); 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) { 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)); btn.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_full));
} }
let is_empty = self.digits.is_empty(); let is_empty = self.digits.is_empty();
self.reset_btn let cancel_enabled = is_empty && self.allow_cancel;
.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty)); self.reset_btn.mutate(ctx, |ctx, btn| {
self.cancel_btn btn.show_if(ctx, !is_empty);
.mutate(ctx, |ctx, btn| btn.enable_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 self.confirm_btn
.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty)); .mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty));
let digit_count = self.digits.len(); 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; type Msg = PinKeyboardMsg;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
// Prompts and PIN dots display. // Prompts and PIN dots display.
let grid = if self.minor_prompt.text().is_empty() { let (header, keypad) = bounds
// Make the major prompt bigger if the minor one is empty. .inset(theme::borders())
Grid::new(bounds, 5, 1) .split_top(Self::HEADER_HEIGHT + Self::HEADER_PADDING_BOTTOM);
} else { let header = header.inset(Insets::new(
Grid::new(bounds, 6, 1) 0,
}; Self::HEADER_PADDING_SIDE,
self.major_prompt.place(grid.row_col(0, 0)); Self::HEADER_PADDING_BOTTOM,
self.minor_prompt.place(grid.row_col(0, 1)); Self::HEADER_PADDING_SIDE,
self.dots.place(grid.row_col(0, 0)); ));
let major_area = header.translate(Self::MAJOR_OFF);
let minor_area = header.translate(Self::MINOR_OFF);
// Control buttons. // Control buttons.
let grid = Grid::new(bounds, 5, 3); let grid = Grid::new(keypad, 4, 3).with_spacing(theme::KEYBOARD_SPACING);
self.reset_btn.place(grid.row_col(4, 0));
self.cancel_btn.place(grid.row_col(4, 0)); // Prompts and PIN dots display.
self.confirm_btn.place(grid.row_col(4, 2)); 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. // Digit buttons.
for (i, btn) in self.digit_btns.iter_mut().enumerate() { 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 { let area = grid.cell(if i < 9 {
// The grid has 3 columns, and we skip the first row. i
i + 3
} else { } else {
// For the last key (the "0" position) we skip one cell. // For the last key (the "0" position) we skip one cell.
i + 1 + 3 i + 1
}); });
btn.place(area); btn.place(area);
} }
@ -151,12 +203,17 @@ impl Component for PinKeyboard {
} }
fn paint(&mut self) { fn paint(&mut self) {
if self.digits.is_empty() {
self.cancel_btn.paint();
self.major_prompt.paint();
self.minor_prompt.paint();
} else {
self.reset_btn.paint(); self.reset_btn.paint();
if self.digits.is_empty() {
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.dots.paint(); self.dots.paint();
} }
self.confirm_btn.paint(); self.confirm_btn.paint();
@ -164,6 +221,18 @@ impl Component for PinKeyboard {
btn.paint(); 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 { struct PinDots {
@ -173,7 +242,7 @@ struct PinDots {
} }
impl PinDots { impl PinDots {
const DOT: i32 = 10; const DOT: i32 = 6;
const PADDING: i32 = 4; const PADDING: i32 = 4;
fn new(digit_count: usize, style: LabelStyle) -> Self { fn new(digit_count: usize, style: LabelStyle) -> Self {
@ -190,6 +259,17 @@ impl PinDots {
ctx.request_paint(); 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 { impl Component for PinDots {
@ -205,22 +285,36 @@ impl Component for PinDots {
} }
fn paint(&mut self) { fn paint(&mut self) {
// Clear the area with the background color. self.clear();
display::rect_fill(self.area, self.style.background_color);
let mut cursor = self
.size()
.snap(self.area.center(), Alignment::Center, Alignment::Center);
// Draw a dot for each PIN digit. // Draw a dot for each PIN digit.
for i in 0..self.digit_count { for _ in 0..self.digit_count {
let pos = Point { display::icon_top_left(
x: self.area.x0 + i as i32 * (Self::DOT + Self::PADDING), cursor,
y: self.area.center().y, theme::DOT_ACTIVE,
};
let size = Offset::new(Self::DOT, Self::DOT);
display::rect_fill_rounded(
Rect::from_top_left_and_size(pos, size),
self.style.text_color, self.style.text_color,
self.style.background_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();
}
} }

View File

@ -11,6 +11,7 @@ pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet
pub use confirm::{HoldToConfirm, HoldToConfirmMsg}; pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
pub use dialog::{Dialog, DialogLayout, DialogMsg}; pub use dialog::{Dialog, DialogLayout, DialogMsg};
pub use frame::Frame; pub use frame::Frame;
pub use keyboard::pin::{PinKeyboard, PinKeyboardMsg};
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet}; pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use page::SwipePage; pub use page::SwipePage;
pub use swipe::{Swipe, SwipeDirection}; pub use swipe::{Swipe, SwipeDirection};

View File

@ -189,8 +189,6 @@ impl ScrollBar {
/// Edge of last dot to center of arrow icon. /// Edge of last dot to center of arrow icon.
const ARROW_SPACE: i32 = 26; 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_UP: &'static [u8] = include_res!("model_tt/res/scroll-up.toif");
const ICON_DOWN: &'static [u8] = include_res!("model_tt/res/scroll-down.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 top = None;
let mut display_icon = |top_left| { let mut display_icon = |top_left| {
let icon = if i == self.active_page { let icon = if i == self.active_page {
Self::ICON_ACTIVE theme::DOT_ACTIVE
} else { } else {
Self::ICON_INACTIVE theme::DOT_INACTIVE
}; };
display::icon_top_left(top_left, icon, theme::FG, theme::BG); display::icon_top_left(top_left, icon, theme::FG, theme::BG);
i += 1; i += 1;

View File

@ -12,11 +12,10 @@ use crate::{
use super::{ use super::{
component::{ component::{
Bip39Input, Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, PinKeyboard,
MnemonicKeyboard, MnemonicKeyboardMsg, PassphraseKeyboard, PassphraseKeyboardMsg, PinKeyboardMsg, SwipePage,
PinKeyboard, PinKeyboardMsg, Slip39Input, SwipePage,
}, },
theme, constant, theme,
}; };
impl<T> TryFrom<DialogMsg<T, ButtonMsg, ButtonMsg>> for Obj 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] #[no_mangle]
extern "C" fn ui_layout_new_example(_param: Obj) -> Obj { extern "C" fn ui_layout_new_example(_param: Obj) -> Obj {
let block = move || { let block = move || {
@ -112,6 +122,22 @@ extern "C" fn ui_layout_new_confirm_action(
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) } unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
} }
#[no_mangle]
extern "C" fn ui_layout_new_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)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{

Binary file not shown.

View File

@ -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 FG: Color = WHITE; // Default foreground (text & icon) color.
pub const BG: Color = BLACK; // Default background color. pub const BG: Color = BLACK; // Default background color.
pub const RED: Color = Color::rgb(205, 73, 73); // dark-coral 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: 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: 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 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_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). // Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2; 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_CANCEL: &[u8] = include_res!("model_tt/res/cancel.toif");
pub const ICON_CONFIRM: &[u8] = include_res!("model_tt/res/confirm.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_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_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 { pub fn label_default() -> LabelStyle {
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 { pub fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet { ButtonStyleSheet {
normal: &ButtonStyle { normal: &ButtonStyle {
@ -99,8 +133,8 @@ pub fn button_confirm() -> ButtonStyleSheet {
}, },
active: &ButtonStyle { active: &ButtonStyle {
font: FONT_BOLD, font: FONT_BOLD,
text_color: BG, text_color: FG,
button_color: FG, button_color: GREEN_DARK,
background_color: BG, background_color: BG,
border_color: FG, border_color: FG,
border_radius: RADIUS, border_radius: RADIUS,
@ -108,7 +142,7 @@ pub fn button_confirm() -> ButtonStyleSheet {
}, },
disabled: &ButtonStyle { disabled: &ButtonStyle {
font: FONT_BOLD, font: FONT_BOLD,
text_color: GREY_LIGHT, text_color: FG,
button_color: GREEN, button_color: GREEN,
background_color: BG, background_color: BG,
border_color: BG, border_color: BG,
@ -119,7 +153,99 @@ pub fn button_confirm() -> ButtonStyleSheet {
} }
pub fn button_cancel() -> 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 { pub fn button_clear() -> ButtonStyleSheet {
@ -159,6 +285,7 @@ impl DefaultTextTheme for TTDefaultText {
} }
pub const CONTENT_BORDER: i32 = 5; pub const CONTENT_BORDER: i32 = 5;
pub const KEYBOARD_SPACING: i32 = 8;
/// +----------+ /// +----------+
/// | 13 | /// | 13 |

View File

@ -20,6 +20,17 @@ def layout_new_example(text: str) -> object:
"""Example layout.""" """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 # extmod/rustmods/modtrezorui2.c
def layout_new_confirm_text( def layout_new_confirm_text(
*, *,

View File

@ -3,10 +3,11 @@ from typing import TYPE_CHECKING
from trezor import io, log, loop, ui, wire, workflow from trezor import io, log, loop, ui, wire, workflow
from trezor.enums import ButtonRequestType from trezor.enums import ButtonRequestType
from trezorui2 import layout_new_confirm_action from trezorui2 import layout_new_confirm_action, layout_new_pin
from ...components.tt import pin
from ...constants.tt import MONO_ADDR_PER_LINE from ...constants.tt import MONO_ADDR_PER_LINE
from ..common import interact from ..common import button_request, interact
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Awaitable, Iterable, NoReturn, Sequence from typing import Any, Awaitable, Iterable, NoReturn, Sequence
@ -451,4 +452,26 @@ async def request_pin_on_device(
attempts_remaining: int | None, attempts_remaining: int | None,
allow_cancel: bool, allow_cancel: bool,
) -> str: ) -> 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