1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-05-06 08:59:15 +00:00

feat(core/rust/ui): show PIN digits when touched

[no changelog]
This commit is contained in:
Martin Milata 2022-04-19 21:54:26 +02:00
parent 5e6582a3fe
commit e58736f746

View File

@ -1,4 +1,4 @@
use core::ops::Deref; use core::{mem, ops::Deref};
use heapless::String; use heapless::String;
use crate::{ use crate::{
@ -10,9 +10,12 @@ use crate::{
}, },
display, display,
geometry::{Alignment, Grid, Insets, Offset, Rect}, geometry::{Alignment, Grid, Insets, Offset, Rect},
model_tt::component::{ model_tt::{
button::{Button, ButtonContent, ButtonMsg::Clicked}, component::{
theme, button::{Button, ButtonContent, ButtonMsg::Clicked},
theme,
},
event::TouchEvent,
}, },
}, },
}; };
@ -25,13 +28,23 @@ 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
const HEADER_HEIGHT: i32 = 25;
const HEADER_PADDING_SIDE: i32 = 5;
const HEADER_PADDING_BOTTOM: i32 = 12;
const HEADER_PADDING: Insets = Insets::new(
theme::borders().top,
HEADER_PADDING_SIDE,
HEADER_PADDING_BOTTOM,
HEADER_PADDING_SIDE,
);
pub struct PinKeyboard<T> { pub struct PinKeyboard<T> {
digits: String<MAX_LENGTH>,
allow_cancel: bool, allow_cancel: bool,
major_prompt: Label<T>, major_prompt: Label<T>,
minor_prompt: Label<T>, minor_prompt: Label<T>,
major_warning: Option<Label<T>>, major_warning: Option<Label<T>>,
dots: Child<PinDots>, textbox: Child<PinDots>,
reset_btn: Child<Maybe<Button<&'static str>>>, reset_btn: Child<Maybe<Button<&'static str>>>,
cancel_btn: Child<Maybe<Button<&'static str>>>, cancel_btn: Child<Maybe<Button<&'static str>>>,
confirm_btn: Child<Button<&'static str>>, confirm_btn: Child<Button<&'static str>>,
@ -42,10 +55,6 @@ impl<T> PinKeyboard<T>
where where
T: Deref<Target = str>, T: Deref<Target = str>,
{ {
const HEADER_HEIGHT: i32 = 25;
const HEADER_PADDING_SIDE: i32 = 5;
const HEADER_PADDING_BOTTOM: i32 = 12;
// Label position fine-tuning. // Label position fine-tuning.
const MAJOR_OFF: Offset = Offset::y(-2); const MAJOR_OFF: Offset = Offset::y(-2);
const MINOR_OFF: Offset = Offset::y(-1); const MINOR_OFF: Offset = Offset::y(-1);
@ -56,8 +65,6 @@ where
major_warning: Option<T>, major_warning: Option<T>,
allow_cancel: bool, allow_cancel: bool,
) -> Self { ) -> Self {
let digits = String::new();
// Control buttons. // Control buttons.
let reset_btn = Button::with_icon(theme::ICON_BACK) let reset_btn = Button::with_icon(theme::ICON_BACK)
.styled(theme::button_reset()) .styled(theme::button_reset())
@ -69,13 +76,12 @@ where
Maybe::new(Pad::with_background(theme::BG), cancel_btn, allow_cancel).into_child(); Maybe::new(Pad::with_background(theme::BG), cancel_btn, allow_cancel).into_child();
Self { Self {
digits,
allow_cancel, allow_cancel,
major_prompt: Label::left_aligned(major_prompt, theme::label_keyboard()), major_prompt: Label::left_aligned(major_prompt, theme::label_keyboard()),
minor_prompt: Label::right_aligned(minor_prompt, theme::label_keyboard_minor()), minor_prompt: Label::right_aligned(minor_prompt, theme::label_keyboard_minor()),
major_warning: major_warning major_warning: major_warning
.map(|text| Label::left_aligned(text, theme::label_keyboard_warning())), .map(|text| Label::left_aligned(text, theme::label_keyboard_warning())),
dots: PinDots::new(0, theme::label_default()).into_child(), textbox: PinDots::new(theme::label_default()).into_child(),
reset_btn, reset_btn,
cancel_btn, cancel_btn,
confirm_btn: Button::with_icon(theme::ICON_CONFIRM) confirm_btn: Button::with_icon(theme::ICON_CONFIRM)
@ -97,12 +103,12 @@ where
} }
fn pin_modified(&mut self, ctx: &mut EventCtx) { fn pin_modified(&mut self, ctx: &mut EventCtx) {
let is_full = self.digits.len() == self.digits.capacity(); let is_full = self.textbox.inner().is_full();
let is_empty = self.textbox.inner().is_empty();
let cancel_enabled = is_empty && self.allow_cancel;
for btn in &mut self.digit_btns { for btn in &mut self.digit_btns {
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 cancel_enabled = is_empty && self.allow_cancel;
self.reset_btn.mutate(ctx, |ctx, btn| { self.reset_btn.mutate(ctx, |ctx, btn| {
btn.show_if(ctx, !is_empty); btn.show_if(ctx, !is_empty);
btn.inner_mut().enable_if(ctx, !is_empty); btn.inner_mut().enable_if(ctx, !is_empty);
@ -113,13 +119,10 @@ where
}); });
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();
self.dots
.mutate(ctx, |ctx, dots| dots.update(ctx, digit_count));
} }
pub fn pin(&self) -> &str { pub fn pin(&self) -> &str {
&self.digits &self.textbox.inner().pin()
} }
} }
@ -130,24 +133,24 @@ where
type Msg = PinKeyboardMsg; type Msg = PinKeyboardMsg;
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
// Ignore the top padding for now, we need it to reliably register textbox touch events.
let borders_no_top = Insets {
top: 0,
..theme::borders()
};
// Prompts and PIN dots display. // Prompts and PIN dots display.
let (header, keypad) = bounds let (header, keypad) = bounds
.inset(theme::borders()) .inset(borders_no_top)
.split_top(Self::HEADER_HEIGHT + Self::HEADER_PADDING_BOTTOM); .split_top(theme::borders().top + HEADER_HEIGHT + HEADER_PADDING_BOTTOM);
let header = header.inset(Insets::new( let prompt = header.inset(HEADER_PADDING);
0, let major_area = prompt.translate(Self::MAJOR_OFF);
Self::HEADER_PADDING_SIDE, let minor_area = prompt.translate(Self::MINOR_OFF);
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. // Control buttons.
let grid = Grid::new(keypad, 4, 3).with_spacing(theme::KEYBOARD_SPACING); let grid = Grid::new(keypad, 4, 3).with_spacing(theme::KEYBOARD_SPACING);
// Prompts and PIN dots display. // Prompts and PIN dots display.
self.dots.place(header); self.textbox.place(header);
self.major_prompt.place(major_area); self.major_prompt.place(major_area);
self.minor_prompt.place(minor_area); self.minor_prompt.place(minor_area);
self.major_warning.as_mut().map(|c| c.place(major_area)); self.major_warning.as_mut().map(|c| c.place(major_area));
@ -174,6 +177,7 @@ where
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
self.textbox.event(ctx, event);
if let Some(Clicked) = self.confirm_btn.event(ctx, event) { if let Some(Clicked) = self.confirm_btn.event(ctx, event) {
return Some(PinKeyboardMsg::Confirmed); return Some(PinKeyboardMsg::Confirmed);
} }
@ -181,17 +185,14 @@ where
return Some(PinKeyboardMsg::Cancelled); return Some(PinKeyboardMsg::Cancelled);
} }
if let Some(Clicked) = self.reset_btn.event(ctx, event) { if let Some(Clicked) = self.reset_btn.event(ctx, event) {
self.digits.clear(); self.textbox.mutate(ctx, |ctx, t| t.clear(ctx));
self.pin_modified(ctx); self.pin_modified(ctx);
return None; return None;
} }
for btn in &mut self.digit_btns { for btn in &mut self.digit_btns {
if let Some(Clicked) = btn.event(ctx, event) { if let Some(Clicked) = btn.event(ctx, event) {
if let ButtonContent::Text(text) = btn.inner().content() { if let ButtonContent::Text(text) = btn.inner().content() {
if self.digits.push_str(text).is_err() { self.textbox.mutate(ctx, |ctx, t| t.push(ctx, text));
// `self.pin` is full and wasn't able to accept all of
// `text`. Should not happen.
}
self.pin_modified(ctx); self.pin_modified(ctx);
return None; return None;
} }
@ -202,8 +203,8 @@ where
fn paint(&mut self) { fn paint(&mut self) {
self.reset_btn.paint(); self.reset_btn.paint();
if self.digits.is_empty() { if self.textbox.inner().is_empty() {
self.dots.inner().clear(); self.textbox.inner().clear_background();
if let Some(ref mut w) = self.major_warning { if let Some(ref mut w) = self.major_warning {
w.paint(); w.paint();
} else { } else {
@ -212,7 +213,7 @@ where
self.minor_prompt.paint(); self.minor_prompt.paint();
self.cancel_btn.paint(); self.cancel_btn.paint();
} else { } else {
self.dots.paint(); self.textbox.paint();
} }
self.confirm_btn.paint(); self.confirm_btn.paint();
for btn in &mut self.digit_btns { for btn in &mut self.digit_btns {
@ -226,7 +227,7 @@ where
self.reset_btn.bounds(sink); self.reset_btn.bounds(sink);
self.cancel_btn.bounds(sink); self.cancel_btn.bounds(sink);
self.confirm_btn.bounds(sink); self.confirm_btn.bounds(sink);
self.dots.bounds(sink); self.textbox.bounds(sink);
for b in &self.digit_btns { for b in &self.digit_btns {
b.bounds(sink) b.bounds(sink)
} }
@ -236,38 +237,93 @@ where
struct PinDots { struct PinDots {
area: Rect, area: Rect,
style: LabelStyle, style: LabelStyle,
digit_count: usize, digits: String<MAX_LENGTH>,
display_digits: bool,
} }
impl PinDots { impl PinDots {
const DOT: i32 = 6; const DOT: i32 = 6;
const PADDING: i32 = 4; const PADDING: i32 = 4;
fn new(digit_count: usize, style: LabelStyle) -> Self { fn new(style: LabelStyle) -> Self {
Self { Self {
style,
digit_count,
area: Rect::zero(), area: Rect::zero(),
} style,
} digits: String::new(),
display_digits: false,
fn update(&mut self, ctx: &mut EventCtx, digit_count: usize) {
if self.digit_count != digit_count {
self.digit_count = digit_count;
ctx.request_paint();
} }
} }
/// Clear the area with the background color. /// Clear the area with the background color.
fn clear(&self) { fn clear_background(&self) {
display::rect_fill(self.area, self.style.background_color); display::rect_fill(self.area, self.style.background_color);
} }
fn size(&self) -> Offset { fn size(&self) -> Offset {
let mut width = Self::DOT * (self.digit_count as i32); let digit_count = self.digits.len();
width += Self::PADDING * (self.digit_count.saturating_sub(1) as i32); let mut width = Self::DOT * (digit_count as i32);
width += Self::PADDING * (digit_count.saturating_sub(1) as i32);
Offset::new(width, Self::DOT) Offset::new(width, Self::DOT)
} }
fn is_empty(&self) -> bool {
self.digits.is_empty()
}
fn is_full(&self) -> bool {
self.digits.len() == self.digits.capacity()
}
fn clear(&mut self, ctx: &mut EventCtx) {
self.digits.clear();
ctx.request_paint()
}
fn push(&mut self, ctx: &mut EventCtx, text: &str) {
if self.digits.push_str(text).is_err() {
// `self.pin` is full and wasn't able to accept all of
// `text`. Should not happen.
};
ctx.request_paint()
}
fn pop(&mut self, ctx: &mut EventCtx) {
if self.digits.pop().is_some() {
ctx.request_paint()
}
}
fn pin(&self) -> &str {
&self.digits
}
fn paint_digits(&self, area: Rect) {
let center = area.center() + Offset::y(theme::FONT_MONO.text_height() / 2);
display::text_center(
center,
&self.digits,
theme::FONT_MONO,
self.style.text_color,
self.style.background_color,
);
}
fn paint_dots(&self, area: Rect) {
let mut cursor = self
.size()
.snap(area.center(), Alignment::Center, Alignment::Center);
// Draw a dot for each PIN digit.
for _ in 0..self.digits.len() {
display::icon_top_left(
cursor,
theme::DOT_ACTIVE,
self.style.text_color,
self.style.background_color,
);
cursor.x += Self::DOT + Self::PADDING;
}
}
} }
impl Component for PinDots { impl Component for PinDots {
@ -278,31 +334,39 @@ impl Component for PinDots {
self.area self.area
} }
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
None match event {
Event::Touch(TouchEvent::TouchStart(pos)) => {
if self.area.contains(pos) {
self.display_digits = true;
ctx.request_paint();
};
None
}
Event::Touch(TouchEvent::TouchEnd(_)) => {
if mem::replace(&mut self.display_digits, false) {
ctx.request_paint();
};
None
}
_ => None,
}
} }
fn paint(&mut self) { fn paint(&mut self) {
self.clear(); self.clear_background();
let dot_area = self.area.inset(HEADER_PADDING);
let mut cursor = self if self.display_digits {
.size() self.paint_digits(dot_area)
.snap(self.area.center(), Alignment::Center, Alignment::Center); } else {
self.paint_dots(dot_area)
// Draw a dot for each PIN digit.
for _ in 0..self.digit_count {
display::icon_top_left(
cursor,
theme::DOT_ACTIVE,
self.style.text_color,
self.style.background_color,
);
cursor.x += Self::DOT + Self::PADDING;
} }
} }
fn bounds(&self, sink: &mut dyn FnMut(Rect)) { fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
sink(self.area); sink(self.area);
sink(self.area.inset(HEADER_PADDING));
} }
} }