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:
parent
5e6582a3fe
commit
e58736f746
@ -1,4 +1,4 @@
|
|||||||
use core::ops::Deref;
|
use core::{mem, ops::Deref};
|
||||||
use heapless::String;
|
use heapless::String;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -10,10 +10,13 @@ use crate::{
|
|||||||
},
|
},
|
||||||
display,
|
display,
|
||||||
geometry::{Alignment, Grid, Insets, Offset, 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,
|
||||||
},
|
},
|
||||||
|
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> {
|
||||||
|
match event {
|
||||||
|
Event::Touch(TouchEvent::TouchStart(pos)) => {
|
||||||
|
if self.area.contains(pos) {
|
||||||
|
self.display_digits = true;
|
||||||
|
ctx.request_paint();
|
||||||
|
};
|
||||||
None
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user