mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-24 23:38:09 +00:00
feat(core/mercury): pin entry animation
[no changelog]
This commit is contained in:
parent
b99325a764
commit
ff869dd864
1
core/.changelog.d/3885.added
Normal file
1
core/.changelog.d/3885.added
Normal file
@ -0,0 +1 @@
|
|||||||
|
[T3T1] Added PIN keyboard animation
|
@ -2,24 +2,29 @@ use core::mem;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
strutil::{ShortString, TString},
|
strutil::{ShortString, TString},
|
||||||
time::Duration,
|
time::{Duration, Stopwatch},
|
||||||
trezorhal::random,
|
trezorhal::random,
|
||||||
ui::{
|
ui::{
|
||||||
component::{
|
component::{
|
||||||
base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe,
|
base::{AttachType, ComponentExt},
|
||||||
Never, Pad, TimerToken,
|
text::TextStyle,
|
||||||
|
Child, Component, Event, EventCtx, Label, Never, Pad, SwipeDirection, TimerToken,
|
||||||
},
|
},
|
||||||
display::Font,
|
display::Font,
|
||||||
event::TouchEvent,
|
event::TouchEvent,
|
||||||
geometry::{Alignment, Alignment2D, Grid, Insets, Offset, Rect},
|
geometry::{Alignment, Alignment2D, Grid, Insets, Offset, Rect},
|
||||||
model_mercury::component::{
|
model_mercury::{
|
||||||
|
component::{
|
||||||
button::{
|
button::{
|
||||||
Button, ButtonContent,
|
Button, ButtonContent,
|
||||||
ButtonMsg::{self, Clicked},
|
ButtonMsg::{self, Clicked},
|
||||||
},
|
},
|
||||||
theme,
|
theme,
|
||||||
},
|
},
|
||||||
|
cshape,
|
||||||
|
},
|
||||||
shape::{self, Renderer},
|
shape::{self, Renderer},
|
||||||
|
util::animation_disabled,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -44,18 +49,217 @@ const HEADER_PADDING: Insets = Insets::new(
|
|||||||
HEADER_PADDING_SIDE,
|
HEADER_PADDING_SIDE,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct AttachAnimation {
|
||||||
|
pub attach_top: bool,
|
||||||
|
pub timer: Stopwatch,
|
||||||
|
pub active: bool,
|
||||||
|
pub duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttachAnimation {
|
||||||
|
const DURATION_MS: u32 = 750;
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
if animation_disabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timer.is_running_within(self.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval(&self) -> f32 {
|
||||||
|
if animation_disabled() {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timer.elapsed().to_millis() as f32 / 1000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opacity(&self, t: f32, pos_x: usize, pos_y: usize) -> u8 {
|
||||||
|
if animation_disabled() {
|
||||||
|
return 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diag = pos_x + pos_y;
|
||||||
|
|
||||||
|
let start = diag as f32 * 0.05;
|
||||||
|
|
||||||
|
let f = pareen::constant(0.0)
|
||||||
|
.seq_ease_in_out(
|
||||||
|
start,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
0.1,
|
||||||
|
pareen::constant(1.0).eval(self.eval()),
|
||||||
|
)
|
||||||
|
.eval(t);
|
||||||
|
|
||||||
|
(f * 255.0) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header_opacity(&self, t: f32) -> u8 {
|
||||||
|
if animation_disabled() {
|
||||||
|
return 255;
|
||||||
|
}
|
||||||
|
let f = pareen::constant(0.0)
|
||||||
|
.seq_ease_in_out(
|
||||||
|
0.65,
|
||||||
|
easer::functions::Linear,
|
||||||
|
0.1,
|
||||||
|
pareen::constant(1.0).eval(self.eval()),
|
||||||
|
)
|
||||||
|
.eval(t);
|
||||||
|
|
||||||
|
(f * 255.0) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&mut self) {
|
||||||
|
self.active = true;
|
||||||
|
self.timer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.active = false;
|
||||||
|
self.timer = Stopwatch::new_stopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lazy_start(&mut self, ctx: &mut EventCtx, event: Event) {
|
||||||
|
if let Event::Attach(_) = event {
|
||||||
|
if let Event::Attach(AttachType::Swipe(SwipeDirection::Up))
|
||||||
|
| Event::Attach(AttachType::Swipe(SwipeDirection::Down))
|
||||||
|
| Event::Attach(AttachType::Initial) = event
|
||||||
|
{
|
||||||
|
self.attach_top = true;
|
||||||
|
self.duration = Duration::from_millis(Self::DURATION_MS);
|
||||||
|
} else {
|
||||||
|
self.duration = Duration::from_millis(Self::DURATION_MS);
|
||||||
|
}
|
||||||
|
self.reset();
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
}
|
||||||
|
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
|
||||||
|
if !self.timer.is_running() {
|
||||||
|
self.start();
|
||||||
|
}
|
||||||
|
if self.is_active() {
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
} else if self.active {
|
||||||
|
self.active = false;
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct CloseAnimation {
|
||||||
|
pub attach_top: bool,
|
||||||
|
pub timer: Stopwatch,
|
||||||
|
pub duration: Duration,
|
||||||
|
}
|
||||||
|
impl CloseAnimation {
|
||||||
|
const DURATION_MS: u32 = 350;
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
if animation_disabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timer.is_running_within(self.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_finished(&self) -> bool {
|
||||||
|
if animation_disabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timer.is_running() && !self.timer.is_running_within(self.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval(&self) -> f32 {
|
||||||
|
if animation_disabled() {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.timer.elapsed().to_millis() as f32 / 1000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opacity(&self, t: f32, pos_x: usize, pos_y: usize) -> u8 {
|
||||||
|
if animation_disabled() {
|
||||||
|
return 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
let diag = pos_x + pos_y;
|
||||||
|
|
||||||
|
let start = diag as f32 * 0.05;
|
||||||
|
|
||||||
|
let f = pareen::constant(1.0)
|
||||||
|
.seq_ease_in_out(
|
||||||
|
start,
|
||||||
|
easer::functions::Cubic,
|
||||||
|
0.1,
|
||||||
|
pareen::constant(0.0).eval(self.eval()),
|
||||||
|
)
|
||||||
|
.eval(t);
|
||||||
|
|
||||||
|
(f * 255.0) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header_opacity(&self, t: f32) -> u8 {
|
||||||
|
if animation_disabled() {
|
||||||
|
return 255;
|
||||||
|
}
|
||||||
|
let f = pareen::constant(1.0)
|
||||||
|
.seq_ease_in_out(
|
||||||
|
0.10,
|
||||||
|
easer::functions::Linear,
|
||||||
|
0.25,
|
||||||
|
pareen::constant(0.0).eval(self.eval()),
|
||||||
|
)
|
||||||
|
.eval(t);
|
||||||
|
|
||||||
|
(f * 255.0) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.timer = Stopwatch::new_stopped();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&mut self, ctx: &mut EventCtx) {
|
||||||
|
self.duration = Duration::from_millis(Self::DURATION_MS);
|
||||||
|
self.reset();
|
||||||
|
self.timer.start();
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
fn process(&mut self, ctx: &mut EventCtx, event: Event) {
|
||||||
|
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
|
||||||
|
if self.is_active() && !self.is_finished() {
|
||||||
|
ctx.request_anim_frame();
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PinKeyboard<'a> {
|
pub struct PinKeyboard<'a> {
|
||||||
allow_cancel: bool,
|
allow_cancel: bool,
|
||||||
|
show_erase: bool,
|
||||||
|
show_cancel: bool,
|
||||||
major_prompt: Child<Label<'a>>,
|
major_prompt: Child<Label<'a>>,
|
||||||
minor_prompt: Child<Label<'a>>,
|
minor_prompt: Child<Label<'a>>,
|
||||||
major_warning: Option<Child<Label<'a>>>,
|
major_warning: Option<Child<Label<'a>>>,
|
||||||
|
keypad_area: Rect,
|
||||||
|
textbox_area: Rect,
|
||||||
textbox: Child<PinDots>,
|
textbox: Child<PinDots>,
|
||||||
textbox_pad: Pad,
|
erase_btn: Button,
|
||||||
erase_btn: Child<Maybe<Button>>,
|
cancel_btn: Button,
|
||||||
cancel_btn: Child<Maybe<Button>>,
|
confirm_btn: Button,
|
||||||
confirm_btn: Child<Button>,
|
digit_btns: [(Button, usize); DIGIT_COUNT],
|
||||||
digit_btns: [Child<Button>; DIGIT_COUNT],
|
|
||||||
warning_timer: Option<TimerToken>,
|
warning_timer: Option<TimerToken>,
|
||||||
|
attach_animation: AttachAnimation,
|
||||||
|
close_animation: CloseAnimation,
|
||||||
|
close_confirm: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> PinKeyboard<'a> {
|
impl<'a> PinKeyboard<'a> {
|
||||||
@ -70,34 +274,37 @@ impl<'a> PinKeyboard<'a> {
|
|||||||
.styled(theme::button_keyboard_erase())
|
.styled(theme::button_keyboard_erase())
|
||||||
.with_long_press(theme::ERASE_HOLD_DURATION)
|
.with_long_press(theme::ERASE_HOLD_DURATION)
|
||||||
.initially_enabled(false);
|
.initially_enabled(false);
|
||||||
let erase_btn = Maybe::hidden(theme::BG, erase_btn).into_child();
|
|
||||||
|
|
||||||
let cancel_btn =
|
let cancel_btn =
|
||||||
Button::with_icon(theme::ICON_CLOSE).styled(theme::button_keyboard_cancel());
|
Button::with_icon(theme::ICON_CLOSE).styled(theme::button_keyboard_cancel());
|
||||||
let cancel_btn = Maybe::new(theme::BG, cancel_btn, allow_cancel).into_child();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
allow_cancel,
|
allow_cancel,
|
||||||
|
show_erase: false,
|
||||||
|
show_cancel: allow_cancel,
|
||||||
major_prompt: Label::left_aligned(major_prompt, theme::label_keyboard()).into_child(),
|
major_prompt: Label::left_aligned(major_prompt, theme::label_keyboard()).into_child(),
|
||||||
minor_prompt: Label::right_aligned(minor_prompt, theme::label_keyboard_minor())
|
minor_prompt: Label::right_aligned(minor_prompt, theme::label_keyboard_minor())
|
||||||
.into_child(),
|
.into_child(),
|
||||||
major_warning: major_warning.map(|text| {
|
major_warning: major_warning.map(|text| {
|
||||||
Label::left_aligned(text, theme::label_keyboard_warning()).into_child()
|
Label::left_aligned(text, theme::label_keyboard_warning()).into_child()
|
||||||
}),
|
}),
|
||||||
|
keypad_area: Rect::zero(),
|
||||||
|
textbox_area: Rect::zero(),
|
||||||
textbox: PinDots::new(theme::label_default()).into_child(),
|
textbox: PinDots::new(theme::label_default()).into_child(),
|
||||||
textbox_pad: Pad::with_background(theme::label_default().background_color),
|
|
||||||
erase_btn,
|
erase_btn,
|
||||||
cancel_btn,
|
cancel_btn,
|
||||||
confirm_btn: Button::with_icon(theme::ICON_SIMPLE_CHECKMARK24)
|
confirm_btn: Button::with_icon(theme::ICON_SIMPLE_CHECKMARK24)
|
||||||
.styled(theme::button_pin_confirm())
|
.styled(theme::button_pin_confirm())
|
||||||
.initially_enabled(false)
|
.initially_enabled(false),
|
||||||
.into_child(),
|
|
||||||
digit_btns: Self::generate_digit_buttons(),
|
digit_btns: Self::generate_digit_buttons(),
|
||||||
warning_timer: None,
|
warning_timer: None,
|
||||||
|
attach_animation: AttachAnimation::default(),
|
||||||
|
close_animation: CloseAnimation::default(),
|
||||||
|
close_confirm: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_digit_buttons() -> [Child<Button>; DIGIT_COUNT] {
|
fn generate_digit_buttons() -> [(Button, usize); DIGIT_COUNT] {
|
||||||
// Generate a random sequence of digits from 0 to 9.
|
// Generate a random sequence of digits from 0 to 9.
|
||||||
let mut digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
let mut digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
||||||
random::shuffle(&mut digits);
|
random::shuffle(&mut digits);
|
||||||
@ -107,14 +314,13 @@ impl<'a> PinKeyboard<'a> {
|
|||||||
b.styled(theme::button_keyboard())
|
b.styled(theme::button_keyboard())
|
||||||
.with_text_align(Alignment::Center)
|
.with_text_align(Alignment::Center)
|
||||||
})
|
})
|
||||||
.map(Child::new)
|
.map(|b| (b, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pin_modified(&mut self, ctx: &mut EventCtx) {
|
fn pin_modified(&mut self, ctx: &mut EventCtx) {
|
||||||
let is_full = self.textbox.inner().is_full();
|
let is_full = self.textbox.inner().is_full();
|
||||||
let is_empty = self.textbox.inner().is_empty();
|
let is_empty = self.textbox.inner().is_empty();
|
||||||
|
|
||||||
self.textbox_pad.clear();
|
|
||||||
self.textbox.request_complete_repaint(ctx);
|
self.textbox.request_complete_repaint(ctx);
|
||||||
|
|
||||||
if is_empty {
|
if is_empty {
|
||||||
@ -125,23 +331,32 @@ impl<'a> PinKeyboard<'a> {
|
|||||||
|
|
||||||
let cancel_enabled = is_empty && self.allow_cancel;
|
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.0.enable_if(ctx, !is_full);
|
||||||
}
|
}
|
||||||
self.erase_btn.mutate(ctx, |ctx, btn| {
|
|
||||||
btn.show_if(ctx, !is_empty);
|
self.show_erase = !is_empty;
|
||||||
btn.inner_mut().enable_if(ctx, !is_empty);
|
self.show_cancel = cancel_enabled && is_empty;
|
||||||
});
|
|
||||||
self.cancel_btn.mutate(ctx, |ctx, btn| {
|
self.erase_btn.enable_if(ctx, !is_empty);
|
||||||
btn.show_if(ctx, cancel_enabled);
|
self.cancel_btn.enable_if(ctx, is_empty);
|
||||||
btn.inner_mut().enable_if(ctx, is_empty);
|
self.confirm_btn.enable_if(ctx, !is_empty);
|
||||||
});
|
|
||||||
self.confirm_btn
|
|
||||||
.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pin(&self) -> &str {
|
pub fn pin(&self) -> &str {
|
||||||
self.textbox.inner().pin()
|
self.textbox.inner().pin()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_button_alpha(&self, x: usize, y: usize, attach_time: f32, close_time: f32) -> u8 {
|
||||||
|
self.attach_animation
|
||||||
|
.opacity(attach_time, x, y)
|
||||||
|
.min(self.close_animation.opacity(close_time, x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_textbox_alpha(&self, attach_time: f32, close_time: f32) -> u8 {
|
||||||
|
self.attach_animation
|
||||||
|
.header_opacity(attach_time)
|
||||||
|
.min(self.close_animation.header_opacity(close_time))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for PinKeyboard<'_> {
|
impl Component for PinKeyboard<'_> {
|
||||||
@ -153,11 +368,14 @@ impl Component for PinKeyboard<'_> {
|
|||||||
bounds.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
|
bounds.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
|
||||||
let prompt = header.inset(HEADER_PADDING);
|
let prompt = header.inset(HEADER_PADDING);
|
||||||
|
|
||||||
|
// Keypad area.
|
||||||
|
self.keypad_area = keypad;
|
||||||
|
|
||||||
// Control buttons.
|
// Control buttons.
|
||||||
let grid = Grid::new(keypad, 4, 3).with_spacing(theme::BUTTON_SPACING);
|
let grid = Grid::new(keypad, 4, 3).with_spacing(theme::BUTTON_SPACING);
|
||||||
|
|
||||||
// Prompts and PIN dots display.
|
// Prompts and PIN dots display.
|
||||||
self.textbox_pad.place(header);
|
self.textbox_area = header;
|
||||||
self.textbox.place(header);
|
self.textbox.place(header);
|
||||||
self.major_prompt.place(prompt);
|
self.major_prompt.place(prompt);
|
||||||
self.minor_prompt.place(prompt);
|
self.minor_prompt.place(prompt);
|
||||||
@ -172,19 +390,32 @@ impl Component for PinKeyboard<'_> {
|
|||||||
// Digit buttons.
|
// Digit buttons.
|
||||||
for (i, btn) in self.digit_btns.iter_mut().enumerate() {
|
for (i, btn) in self.digit_btns.iter_mut().enumerate() {
|
||||||
// Assign the digits to buttons on a 4x3 grid, starting from the first row.
|
// Assign the digits to buttons on a 4x3 grid, starting from the first row.
|
||||||
let area = grid.cell(if i < 9 {
|
let idx = if i < 9 {
|
||||||
i
|
i
|
||||||
} else {
|
} else {
|
||||||
// For the last key (the "0" position) we skip one cell.
|
// For the last key (the "0" position) we skip one cell.
|
||||||
i + 1
|
i + 1
|
||||||
});
|
};
|
||||||
btn.place(area);
|
let area = grid.cell(idx);
|
||||||
|
btn.0.place(area);
|
||||||
|
btn.1 = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
bounds
|
bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
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.close_animation.process(ctx, event);
|
||||||
|
if self.close_animation.is_finished() && !animation_disabled() {
|
||||||
|
return Some(if self.close_confirm {
|
||||||
|
PinKeyboardMsg::Confirmed
|
||||||
|
} else {
|
||||||
|
PinKeyboardMsg::Cancelled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.attach_animation.lazy_start(ctx, event);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
// Set up timer to switch off warning prompt.
|
// Set up timer to switch off warning prompt.
|
||||||
Event::Attach(_) if self.major_warning.is_some() => {
|
Event::Attach(_) if self.major_warning.is_some() => {
|
||||||
@ -193,19 +424,33 @@ impl Component for PinKeyboard<'_> {
|
|||||||
// Hide warning, show major prompt.
|
// Hide warning, show major prompt.
|
||||||
Event::Timer(token) if Some(token) == self.warning_timer => {
|
Event::Timer(token) if Some(token) == self.warning_timer => {
|
||||||
self.major_warning = None;
|
self.major_warning = None;
|
||||||
self.textbox_pad.clear();
|
|
||||||
self.minor_prompt.request_complete_repaint(ctx);
|
self.minor_prompt.request_complete_repaint(ctx);
|
||||||
ctx.request_paint();
|
ctx.request_paint();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// do not process buttons when closing
|
||||||
|
if self.close_animation.is_active() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
self.textbox.event(ctx, event);
|
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) {
|
||||||
|
if animation_disabled() {
|
||||||
return Some(PinKeyboardMsg::Confirmed);
|
return Some(PinKeyboardMsg::Confirmed);
|
||||||
|
} else {
|
||||||
|
self.close_animation.start(ctx);
|
||||||
|
self.close_confirm = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(Clicked) = self.cancel_btn.event(ctx, event) {
|
if let Some(Clicked) = self.cancel_btn.event(ctx, event) {
|
||||||
|
if animation_disabled() {
|
||||||
return Some(PinKeyboardMsg::Cancelled);
|
return Some(PinKeyboardMsg::Cancelled);
|
||||||
|
} else {
|
||||||
|
self.close_animation.start(ctx);
|
||||||
|
self.close_confirm = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
match self.erase_btn.event(ctx, event) {
|
match self.erase_btn.event(ctx, event) {
|
||||||
Some(ButtonMsg::Clicked) => {
|
Some(ButtonMsg::Clicked) => {
|
||||||
@ -221,8 +466,8 @@ impl Component for PinKeyboard<'_> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
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.0.event(ctx, event) {
|
||||||
if let ButtonContent::Text(text) = btn.inner().content() {
|
if let ButtonContent::Text(text) = btn.0.content() {
|
||||||
text.map(|text| {
|
text.map(|text| {
|
||||||
self.textbox.mutate(ctx, |ctx, t| t.push(ctx, text));
|
self.textbox.mutate(ctx, |ctx, t| t.push(ctx, text));
|
||||||
});
|
});
|
||||||
@ -239,8 +484,14 @@ impl Component for PinKeyboard<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||||
self.erase_btn.render(target);
|
let t_attach = self.attach_animation.eval();
|
||||||
self.textbox_pad.render(target);
|
let t_close = self.close_animation.eval();
|
||||||
|
|
||||||
|
let erase_alpha = self.get_button_alpha(0, 3, t_attach, t_close);
|
||||||
|
|
||||||
|
if self.show_erase {
|
||||||
|
self.erase_btn.render_with_alpha(target, erase_alpha);
|
||||||
|
}
|
||||||
|
|
||||||
if self.textbox.inner().is_empty() {
|
if self.textbox.inner().is_empty() {
|
||||||
if let Some(ref w) = self.major_warning {
|
if let Some(ref w) = self.major_warning {
|
||||||
@ -249,16 +500,28 @@ impl Component for PinKeyboard<'_> {
|
|||||||
self.major_prompt.render(target);
|
self.major_prompt.render(target);
|
||||||
}
|
}
|
||||||
self.minor_prompt.render(target);
|
self.minor_prompt.render(target);
|
||||||
self.cancel_btn.render(target);
|
if self.show_cancel {
|
||||||
|
self.cancel_btn.render_with_alpha(target, erase_alpha);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.textbox.render(target);
|
self.textbox.render(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.confirm_btn.render(target);
|
shape::Bar::new(self.textbox_area)
|
||||||
|
.with_bg(theme::label_default().background_color)
|
||||||
|
.with_fg(theme::label_default().background_color)
|
||||||
|
.with_alpha(255 - self.get_textbox_alpha(t_attach, t_close))
|
||||||
|
.render(target);
|
||||||
|
|
||||||
|
let alpha = self.get_button_alpha(2, 3, t_attach, t_close);
|
||||||
|
self.confirm_btn.render_with_alpha(target, alpha);
|
||||||
|
|
||||||
for btn in &self.digit_btns {
|
for btn in &self.digit_btns {
|
||||||
btn.render(target);
|
let alpha = self.get_button_alpha(btn.1 % 3, btn.1 / 3, t_attach, t_close);
|
||||||
|
btn.0.render_with_alpha(target, alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cshape::KeyboardOverlay::new(self.keypad_area).render(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -439,7 +702,7 @@ impl crate::trace::Trace for PinKeyboard<'_> {
|
|||||||
// So that debuglink knows the locations of the buttons
|
// So that debuglink knows the locations of the buttons
|
||||||
let mut digits_order = ShortString::new();
|
let mut digits_order = ShortString::new();
|
||||||
for btn in self.digit_btns.iter() {
|
for btn in self.digit_btns.iter() {
|
||||||
let btn_content = btn.inner().content();
|
let btn_content = btn.0.content();
|
||||||
if let ButtonContent::Text(text) = btn_content {
|
if let ButtonContent::Text(text) = btn_content {
|
||||||
text.map(|text| {
|
text.map(|text| {
|
||||||
unwrap!(digits_order.push_str(text));
|
unwrap!(digits_order.push_str(text));
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
display::Color,
|
||||||
|
geometry::Rect,
|
||||||
|
shape::{Canvas, DrawingCache, Mono8Canvas, Renderer, Shape, ShapeClone},
|
||||||
|
};
|
||||||
|
|
||||||
|
use without_alloc::alloc::LocalAllocLeakExt;
|
||||||
|
|
||||||
|
/// A special shape for rendering keyboard overlay.
|
||||||
|
/// Makes the corner buttons have rounded corners.
|
||||||
|
pub struct KeyboardOverlay {
|
||||||
|
/// Center of the overlay
|
||||||
|
area: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyboardOverlay {
|
||||||
|
pub const RADIUS: i16 = 6;
|
||||||
|
|
||||||
|
/// Create a new overlay with given area
|
||||||
|
pub fn new(area: Rect) -> Self {
|
||||||
|
Self { area }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<'a>(self, renderer: &mut impl Renderer<'a>) {
|
||||||
|
renderer.render_shape(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_overlay(&self, canvas: &mut dyn Canvas) {
|
||||||
|
let area = canvas.bounds();
|
||||||
|
|
||||||
|
let transp = Color::black();
|
||||||
|
let opaque = Color::white();
|
||||||
|
|
||||||
|
canvas.fill_background(opaque);
|
||||||
|
|
||||||
|
canvas.fill_round_rect(area, 6, transp, 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Shape<'a> for KeyboardOverlay {
|
||||||
|
fn bounds(&self) -> Rect {
|
||||||
|
self.area
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, canvas: &mut dyn Canvas, cache: &DrawingCache<'a>) {
|
||||||
|
let bounds = self.bounds();
|
||||||
|
|
||||||
|
let overlay_buff = &mut unwrap!(cache.image_buff(), "No image buffer");
|
||||||
|
|
||||||
|
let mut overlay_canvas = unwrap!(
|
||||||
|
Mono8Canvas::new(bounds.size(), None, None, &mut overlay_buff[..]),
|
||||||
|
"Too small buffer"
|
||||||
|
);
|
||||||
|
|
||||||
|
self.prepare_overlay(&mut overlay_canvas);
|
||||||
|
|
||||||
|
canvas.blend_bitmap(bounds, overlay_canvas.view().with_fg(Color::black()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(&mut self, _cache: &DrawingCache<'a>) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ShapeClone<'a> for KeyboardOverlay {
|
||||||
|
fn clone_at_bump<T>(self, bump: &'a T) -> Option<&'a mut dyn Shape<'a>>
|
||||||
|
where
|
||||||
|
T: LocalAllocLeakExt<'a>,
|
||||||
|
{
|
||||||
|
let clone = bump.alloc_t()?;
|
||||||
|
Some(clone.uninit.init(KeyboardOverlay { ..self }))
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,10 @@ mod loader;
|
|||||||
|
|
||||||
mod unlock_overlay;
|
mod unlock_overlay;
|
||||||
|
|
||||||
|
mod keyboard_overlay;
|
||||||
|
|
||||||
pub use unlock_overlay::UnlockOverlay;
|
pub use unlock_overlay::UnlockOverlay;
|
||||||
|
|
||||||
|
pub use keyboard_overlay::KeyboardOverlay;
|
||||||
|
|
||||||
pub use loader::{render_loader, LoaderRange};
|
pub use loader::{render_loader, LoaderRange};
|
||||||
|
@ -19,7 +19,7 @@ const ZLIB_CACHE_SLOTS: usize = 3;
|
|||||||
const RENDER_BUFF_SIZE: usize = (240 * 2 * 16) + ALIGN_PAD;
|
const RENDER_BUFF_SIZE: usize = (240 * 2 * 16) + ALIGN_PAD;
|
||||||
|
|
||||||
#[cfg(feature = "model_mercury")]
|
#[cfg(feature = "model_mercury")]
|
||||||
const IMAGE_BUFF_SIZE: usize = 32768 + ALIGN_PAD;
|
const IMAGE_BUFF_SIZE: usize = 240 * 240 + ALIGN_PAD;
|
||||||
#[cfg(not(feature = "model_mercury"))]
|
#[cfg(not(feature = "model_mercury"))]
|
||||||
const IMAGE_BUFF_SIZE: usize = 2048 + ALIGN_PAD;
|
const IMAGE_BUFF_SIZE: usize = 2048 + ALIGN_PAD;
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user