|
|
|
@ -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();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|