From e58736f746cbd3fe9d92c892e088566ebb9f03d4 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Tue, 19 Apr 2022 21:54:26 +0200 Subject: [PATCH] feat(core/rust/ui): show PIN digits when touched [no changelog] --- .../src/ui/model_tt/component/keyboard/pin.rs | 198 ++++++++++++------ 1 file changed, 131 insertions(+), 67 deletions(-) diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs index 88289353e..7d8ce6fbc 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs @@ -1,4 +1,4 @@ -use core::ops::Deref; +use core::{mem, ops::Deref}; use heapless::String; use crate::{ @@ -10,9 +10,12 @@ use crate::{ }, display, geometry::{Alignment, Grid, Insets, Offset, Rect}, - model_tt::component::{ - button::{Button, ButtonContent, ButtonMsg::Clicked}, - theme, + model_tt::{ + component::{ + button::{Button, ButtonContent, ButtonMsg::Clicked}, + theme, + }, + event::TouchEvent, }, }, }; @@ -25,13 +28,23 @@ pub enum PinKeyboardMsg { const MAX_LENGTH: usize = 9; 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 { - digits: String, allow_cancel: bool, major_prompt: Label, minor_prompt: Label, major_warning: Option>, - dots: Child, + textbox: Child, reset_btn: Child>>, cancel_btn: Child>>, confirm_btn: Child>, @@ -42,10 +55,6 @@ impl PinKeyboard where T: Deref, { - 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); @@ -56,8 +65,6 @@ where major_warning: Option, allow_cancel: bool, ) -> Self { - let digits = String::new(); - // Control buttons. let reset_btn = Button::with_icon(theme::ICON_BACK) .styled(theme::button_reset()) @@ -69,13 +76,12 @@ where Maybe::new(Pad::with_background(theme::BG), cancel_btn, allow_cancel).into_child(); Self { - 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(), + textbox: PinDots::new(theme::label_default()).into_child(), reset_btn, cancel_btn, confirm_btn: Button::with_icon(theme::ICON_CONFIRM) @@ -97,12 +103,12 @@ where } 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 { 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| { btn.show_if(ctx, !is_empty); btn.inner_mut().enable_if(ctx, !is_empty); @@ -113,13 +119,10 @@ where }); self.confirm_btn .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 { - &self.digits + &self.textbox.inner().pin() } } @@ -130,24 +133,24 @@ where type Msg = PinKeyboardMsg; 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. 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); + .inset(borders_no_top) + .split_top(theme::borders().top + HEADER_HEIGHT + HEADER_PADDING_BOTTOM); + let prompt = header.inset(HEADER_PADDING); + let major_area = prompt.translate(Self::MAJOR_OFF); + let minor_area = prompt.translate(Self::MINOR_OFF); // Control buttons. let grid = Grid::new(keypad, 4, 3).with_spacing(theme::KEYBOARD_SPACING); // Prompts and PIN dots display. - self.dots.place(header); + self.textbox.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)); @@ -174,6 +177,7 @@ where } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.textbox.event(ctx, event); if let Some(Clicked) = self.confirm_btn.event(ctx, event) { return Some(PinKeyboardMsg::Confirmed); } @@ -181,17 +185,14 @@ where return Some(PinKeyboardMsg::Cancelled); } 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); return None; } for btn in &mut self.digit_btns { if let Some(Clicked) = btn.event(ctx, event) { if let ButtonContent::Text(text) = btn.inner().content() { - if self.digits.push_str(text).is_err() { - // `self.pin` is full and wasn't able to accept all of - // `text`. Should not happen. - } + self.textbox.mutate(ctx, |ctx, t| t.push(ctx, text)); self.pin_modified(ctx); return None; } @@ -202,8 +203,8 @@ where fn paint(&mut self) { self.reset_btn.paint(); - if self.digits.is_empty() { - self.dots.inner().clear(); + if self.textbox.inner().is_empty() { + self.textbox.inner().clear_background(); if let Some(ref mut w) = self.major_warning { w.paint(); } else { @@ -212,7 +213,7 @@ where self.minor_prompt.paint(); self.cancel_btn.paint(); } else { - self.dots.paint(); + self.textbox.paint(); } self.confirm_btn.paint(); for btn in &mut self.digit_btns { @@ -226,7 +227,7 @@ where self.reset_btn.bounds(sink); self.cancel_btn.bounds(sink); self.confirm_btn.bounds(sink); - self.dots.bounds(sink); + self.textbox.bounds(sink); for b in &self.digit_btns { b.bounds(sink) } @@ -236,61 +237,84 @@ where struct PinDots { area: Rect, style: LabelStyle, - digit_count: usize, + digits: String, + display_digits: bool, } impl PinDots { const DOT: i32 = 6; const PADDING: i32 = 4; - fn new(digit_count: usize, style: LabelStyle) -> Self { + fn new(style: LabelStyle) -> Self { Self { - style, - digit_count, area: Rect::zero(), - } - } - - fn update(&mut self, ctx: &mut EventCtx, digit_count: usize) { - if self.digit_count != digit_count { - self.digit_count = digit_count; - ctx.request_paint(); + style, + digits: String::new(), + display_digits: false, } } /// Clear the area with the background color. - fn clear(&self) { + fn clear_background(&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); + let digit_count = self.digits.len(); + let mut width = Self::DOT * (digit_count as i32); + width += Self::PADDING * (digit_count.saturating_sub(1) as i32); Offset::new(width, Self::DOT) } -} -impl Component for PinDots { - type Msg = Never; + fn is_empty(&self) -> bool { + self.digits.is_empty() + } - fn place(&mut self, bounds: Rect) -> Rect { - self.area = bounds; - self.area + fn is_full(&self) -> bool { + self.digits.len() == self.digits.capacity() } - fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { - None + fn clear(&mut self, ctx: &mut EventCtx) { + self.digits.clear(); + ctx.request_paint() } - fn paint(&mut self) { - self.clear(); + 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(self.area.center(), Alignment::Center, Alignment::Center); + .snap(area.center(), Alignment::Center, Alignment::Center); // Draw a dot for each PIN digit. - for _ in 0..self.digit_count { + for _ in 0..self.digits.len() { display::icon_top_left( cursor, theme::DOT_ACTIVE, @@ -300,9 +324,49 @@ impl Component for PinDots { cursor.x += Self::DOT + Self::PADDING; } } +} + +impl Component for PinDots { + type Msg = Never; + + fn place(&mut self, bounds: Rect) -> Rect { + self.area = bounds; + self.area + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + 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) { + self.clear_background(); + let dot_area = self.area.inset(HEADER_PADDING); + + if self.display_digits { + self.paint_digits(dot_area) + } else { + self.paint_dots(dot_area) + } + } fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.area); + sink(self.area.inset(HEADER_PADDING)); } }