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

[no changelog]
mmilata/tt-pin-keyboard
Martin Milata 2 years ago
parent eab791918e
commit bda702d879

@ -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,
/// danger: bool,
/// allow_cancel: bool,
/// ) -> 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_danger;
MP_QSTR_allow_cancel;
}

@ -24,7 +24,7 @@ where
{
pub fn new(origin: Point, align: Alignment, text: T, style: LabelStyle) -> Self {
let width = style.font.text_width(&text);
let height = style.font.line_height();
let height = style.font.text_height();
let area = match align {
// `origin` is the top-left point.
Alignment::Start => Rect {
@ -63,6 +63,11 @@ where
Self::new(origin, Alignment::Center, text, style)
}
pub fn with_text_color(mut self, text_color: Color) -> Self {
self.style.text_color = text_color;
self
}
pub fn text(&self) -> &T {
&self.text
}
@ -87,4 +92,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 {

@ -48,6 +48,13 @@ impl<T> Button<T> {
self
}
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)
}

@ -14,6 +14,7 @@ pub use dialog::{Dialog, DialogLayout, DialogMsg};
pub use frame::Frame;
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
pub use page::SwipePage;
pub use pin::{PinDialog, PinDialogMsg};
pub use swipe::{Swipe, SwipeDirection};
use super::{event, theme};

@ -188,8 +188,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");
@ -242,9 +240,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;

@ -1,3 +1,4 @@
use core::ops::Deref;
use heapless::Vec;
use crate::{
@ -8,8 +9,8 @@ use crate::{
label::{Label, LabelStyle},
Child, Component, Event, EventCtx, Never,
},
display,
geometry::{Grid, Offset, Point, Rect},
display::{self, Color},
geometry::{Alignment, Grid, Insets, Offset, Rect},
},
};
@ -26,10 +27,20 @@ pub enum PinDialogMsg {
const MAX_LENGTH: usize = 9;
const DIGIT_COUNT: usize = 10; // 0..10
pub struct PinDialog {
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 struct PinDialog<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>,
dots: Child<PinDots>,
reset_btn: Child<Button<&'static str>>,
cancel_btn: Child<Button<&'static str>>,
@ -37,40 +48,59 @@ pub struct PinDialog {
digit_btns: [Child<Button<&'static str>>; DIGIT_COUNT],
}
impl PinDialog {
pub fn new(area: Rect, major_prompt: &'static [u8], minor_prompt: &'static [u8]) -> Self {
impl<T> PinDialog<T>
where
T: Deref<Target = [u8]>,
{
pub fn new(
area: Rect,
major_prompt: T,
minor_prompt: T,
major_color: Color,
minor_color: Color,
allow_cancel: bool,
) -> Self {
let area = area.inset(Insets::right(theme::CONTENT_BORDER));
let digits = Vec::new();
// Prompts and PIN dots display.
let grid = if minor_prompt.is_empty() {
// Make the major prompt bigger if the minor one is empty.
Grid::new(area, 5, 1)
} else {
Grid::new(area, 6, 1)
};
let major_prompt = Label::centered(
grid.row_col(0, 0).center(),
let (header, keypad) = area.split_top(HEADER_HEIGHT + HEADER_PADDING_BOTTOM);
let header = header.inset(Insets::new(
0,
HEADER_PADDING_SIDE,
HEADER_PADDING_BOTTOM,
HEADER_PADDING_SIDE,
));
let major_prompt = Label::left_aligned(
header.top_left() + MAJOR_OFF,
major_prompt,
theme::label_default(),
);
let minor_prompt = Label::centered(
grid.row_col(0, 1).center(),
theme::label_medium(),
)
.with_text_color(major_color);
let minor_prompt = Label::right_aligned(
header.top_right() + MINOR_OFF,
minor_prompt,
theme::label_default(),
);
let dots =
PinDots::new(grid.row_col(0, 0), digits.len(), theme::label_default()).into_child();
)
.with_text_color(minor_color);
let dots = PinDots::new(header, digits.len(), theme::label_default()).into_child();
// Control buttons.
let grid = Grid::new(area, 5, 3);
let reset_btn = Button::with_text(grid.row_col(4, 0), "Reset")
.styled(theme::button_clear())
let grid = Grid::new(keypad, 4, 3).with_spacing(BUTTON_SPACING);
let reset_btn = Button::with_icon(grid.row_col(3, 0), theme::ICON_CANCEL)
.styled(theme::button_cancel())
.initially_enabled(false)
.into_child();
let cancel_btn = Button::with_icon(grid.row_col(4, 0), theme::ICON_CANCEL)
let cancel_btn = Button::with_icon(grid.row_col(3, 0), theme::ICON_CANCEL)
.styled(theme::button_cancel())
.initially_enabled(allow_cancel)
.into_child();
let confirm_btn = Button::with_icon(grid.row_col(4, 2), theme::ICON_CONFIRM)
.styled(theme::button_clear())
let confirm_btn = Button::with_icon(grid.row_col(3, 2), theme::ICON_NEXT)
.styled(theme::button_confirm())
.initially_enabled(false)
.into_child();
// PIN digit buttons.
@ -78,6 +108,7 @@ impl PinDialog {
Self {
digits,
allow_cancel,
major_prompt,
minor_prompt,
dots,
@ -97,13 +128,13 @@ impl PinDialog {
let btn = |i| {
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
});
let text = digits[i];
Child::new(Button::with_text(area, text))
Child::new(Button::with_text(area, text).styled(theme::button_pin()))
};
[
btn(0),
@ -125,10 +156,11 @@ impl PinDialog {
btn.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_full));
}
let is_empty = self.digits.is_empty();
let cancel_enabled = is_empty && self.allow_cancel;
self.reset_btn
.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_empty));
self.cancel_btn
.mutate(ctx, |ctx, btn| btn.enabled(ctx, is_empty));
.mutate(ctx, |ctx, btn| btn.enabled(ctx, cancel_enabled));
self.confirm_btn
.mutate(ctx, |ctx, btn| btn.enabled(ctx, !is_empty));
let digit_count = self.digits.len();
@ -141,7 +173,10 @@ impl PinDialog {
}
}
impl Component for PinDialog {
impl<T> Component for PinDialog<T>
where
T: Deref<Target = [u8]>,
{
type Msg = PinDialogMsg;
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
@ -172,12 +207,13 @@ impl Component for PinDialog {
}
fn paint(&mut self) {
self.reset_btn.paint();
if self.digits.is_empty() {
self.cancel_btn.paint();
self.dots.inner().clear();
self.major_prompt.paint();
self.minor_prompt.paint();
self.cancel_btn.paint();
} else {
self.reset_btn.paint();
self.dots.paint();
}
self.confirm_btn.paint();
@ -185,6 +221,18 @@ impl Component for PinDialog {
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 {
@ -194,7 +242,7 @@ struct PinDots {
}
impl PinDots {
const DOT: i32 = 10;
const DOT: i32 = 6;
const PADDING: i32 = 4;
fn new(area: Rect, digit_count: usize, style: LabelStyle) -> Self {
@ -211,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 get_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 {
@ -221,22 +280,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.get_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 PinDialog<T>
where
T: Deref<Target = [u8]>,
{
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.open("PinDialog");
t.close();
}
}

@ -11,7 +11,10 @@ use crate::{
};
use super::{
component::{Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, SwipePage},
component::{
Button, ButtonMsg, DialogMsg, Frame, HoldToConfirm, HoldToConfirmMsg, PinDialog,
PinDialogMsg, SwipePage,
},
constant, theme,
};
@ -48,6 +51,17 @@ where
}
}
impl TryFrom<PinDialogMsg> for Obj {
type Error = Error;
fn try_from(val: PinDialogMsg) -> Result<Self, Self::Error> {
match val {
PinDialogMsg::Confirmed => 1.try_into(),
PinDialogMsg::Cancelled => 2.try_into(),
}
}
}
#[no_mangle]
extern "C" fn ui_layout_new_example(_param: Obj) -> Obj {
let block = move || {
@ -118,6 +132,34 @@ 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 danger: Option<bool> = kwargs.get(Qstr::MP_QSTR_danger)?.try_into_option()?;
let obj = LayoutObj::new(
PinDialog::new(
theme::borders(),
prompt,
subprompt,
theme::OFF_WHITE,
if danger.unwrap_or(false) {
theme::RED
} else {
theme::OFF_WHITE
},
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::{

@ -31,8 +31,9 @@ pub const RED: Color = Color::rgb(205, 73, 73); // dark-coral
pub const YELLOW: Color = Color::rgb(193, 144, 9); // ochre
pub const GREEN: Color = Color::rgb(57, 168, 20); // grass-green
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_DARK: Color = Color::rgb(51, 51, 51); // greyer
// Commonly used corner radius (i.e. for buttons).
pub const RADIUS: u8 = 2;
@ -44,6 +45,11 @@ 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_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 {
@ -53,6 +59,14 @@ pub fn label_default() -> LabelStyle {
}
}
pub fn label_medium() -> LabelStyle {
LabelStyle {
font: FONT_MEDIUM,
text_color: FG,
background_color: BG,
}
}
pub fn button_default() -> ButtonStyleSheet {
ButtonStyleSheet {
normal: &ButtonStyle {
@ -118,7 +132,67 @@ 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: BG,
button_color: FG,
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_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: BG,
button_color: FG,
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 {

@ -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
@ -449,4 +450,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:
danger = False
subprompt = ""
elif attempts_remaining == 1:
danger = True
subprompt = "Last attempt"
else:
danger = attempts_remaining <= 3
subprompt = f"{attempts_remaining} tries left"
dialog = _RustLayout(
layout_new_pin(
prompt=prompt, subprompt=subprompt, allow_cancel=allow_cancel, danger=danger
)
)
while True:
result = await ctx.wait(dialog)
if result is pin.CANCELLED:
raise wire.PinCancelled
assert isinstance(result, str)
return result

Loading…
Cancel
Save