feat(core/mercury): pin entry animation

[no changelog]
pull/4045/head
tychovrahe 3 months ago committed by TychoVrahe
parent b99325a764
commit ff869dd864

@ -0,0 +1 @@
[T3T1] Added PIN keyboard animation

@ -2,24 +2,29 @@ use core::mem;
use crate::{
strutil::{ShortString, TString},
time::Duration,
time::{Duration, Stopwatch},
trezorhal::random,
ui::{
component::{
base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe,
Never, Pad, TimerToken,
base::{AttachType, ComponentExt},
text::TextStyle,
Child, Component, Event, EventCtx, Label, Never, Pad, SwipeDirection, TimerToken,
},
display::Font,
event::TouchEvent,
geometry::{Alignment, Alignment2D, Grid, Insets, Offset, Rect},
model_mercury::component::{
button::{
Button, ButtonContent,
ButtonMsg::{self, Clicked},
model_mercury::{
component::{
button::{
Button, ButtonContent,
ButtonMsg::{self, Clicked},
},
theme,
},
theme,
cshape,
},
shape::{self, Renderer},
util::animation_disabled,
},
};
@ -44,18 +49,217 @@ const HEADER_PADDING: Insets = Insets::new(
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> {
allow_cancel: bool,
show_erase: bool,
show_cancel: bool,
major_prompt: Child<Label<'a>>,
minor_prompt: Child<Label<'a>>,
major_warning: Option<Child<Label<'a>>>,
keypad_area: Rect,
textbox_area: Rect,
textbox: Child<PinDots>,
textbox_pad: Pad,
erase_btn: Child<Maybe<Button>>,
cancel_btn: Child<Maybe<Button>>,
confirm_btn: Child<Button>,
digit_btns: [Child<Button>; DIGIT_COUNT],
erase_btn: Button,
cancel_btn: Button,
confirm_btn: Button,
digit_btns: [(Button, usize); DIGIT_COUNT],
warning_timer: Option<TimerToken>,
attach_animation: AttachAnimation,
close_animation: CloseAnimation,
close_confirm: bool,
}
impl<'a> PinKeyboard<'a> {
@ -70,34 +274,37 @@ impl<'a> PinKeyboard<'a> {
.styled(theme::button_keyboard_erase())
.with_long_press(theme::ERASE_HOLD_DURATION)
.initially_enabled(false);
let erase_btn = Maybe::hidden(theme::BG, erase_btn).into_child();
let cancel_btn =
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 {
allow_cancel,
show_erase: false,
show_cancel: allow_cancel,
major_prompt: Label::left_aligned(major_prompt, theme::label_keyboard()).into_child(),
minor_prompt: Label::right_aligned(minor_prompt, theme::label_keyboard_minor())
.into_child(),
major_warning: major_warning.map(|text| {
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_pad: Pad::with_background(theme::label_default().background_color),
erase_btn,
cancel_btn,
confirm_btn: Button::with_icon(theme::ICON_SIMPLE_CHECKMARK24)
.styled(theme::button_pin_confirm())
.initially_enabled(false)
.into_child(),
.initially_enabled(false),
digit_btns: Self::generate_digit_buttons(),
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.
let mut digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
random::shuffle(&mut digits);
@ -107,14 +314,13 @@ impl<'a> PinKeyboard<'a> {
b.styled(theme::button_keyboard())
.with_text_align(Alignment::Center)
})
.map(Child::new)
.map(|b| (b, 0))
}
fn pin_modified(&mut self, ctx: &mut EventCtx) {
let is_full = self.textbox.inner().is_full();
let is_empty = self.textbox.inner().is_empty();
self.textbox_pad.clear();
self.textbox.request_complete_repaint(ctx);
if is_empty {
@ -125,23 +331,32 @@ impl<'a> PinKeyboard<'a> {
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));
btn.0.enable_if(ctx, !is_full);
}
self.erase_btn.mutate(ctx, |ctx, btn| {
btn.show_if(ctx, !is_empty);
btn.inner_mut().enable_if(ctx, !is_empty);
});
self.cancel_btn.mutate(ctx, |ctx, btn| {
btn.show_if(ctx, cancel_enabled);
btn.inner_mut().enable_if(ctx, is_empty);
});
self.confirm_btn
.mutate(ctx, |ctx, btn| btn.enable_if(ctx, !is_empty));
self.show_erase = !is_empty;
self.show_cancel = cancel_enabled && is_empty;
self.erase_btn.enable_if(ctx, !is_empty);
self.cancel_btn.enable_if(ctx, is_empty);
self.confirm_btn.enable_if(ctx, !is_empty);
}
pub fn pin(&self) -> &str {
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<'_> {
@ -153,11 +368,14 @@ impl Component for PinKeyboard<'_> {
bounds.split_bottom(4 * theme::PIN_BUTTON_HEIGHT + 3 * theme::BUTTON_SPACING);
let prompt = header.inset(HEADER_PADDING);
// Keypad area.
self.keypad_area = keypad;
// Control buttons.
let grid = Grid::new(keypad, 4, 3).with_spacing(theme::BUTTON_SPACING);
// Prompts and PIN dots display.
self.textbox_pad.place(header);
self.textbox_area = header;
self.textbox.place(header);
self.major_prompt.place(prompt);
self.minor_prompt.place(prompt);
@ -172,19 +390,32 @@ impl Component for PinKeyboard<'_> {
// Digit buttons.
for (i, btn) in self.digit_btns.iter_mut().enumerate() {
// 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
} else {
// For the last key (the "0" position) we skip one cell.
i + 1
});
btn.place(area);
};
let area = grid.cell(idx);
btn.0.place(area);
btn.1 = idx;
}
bounds
}
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 {
// Set up timer to switch off warning prompt.
Event::Attach(_) if self.major_warning.is_some() => {
@ -193,19 +424,33 @@ impl Component for PinKeyboard<'_> {
// Hide warning, show major prompt.
Event::Timer(token) if Some(token) == self.warning_timer => {
self.major_warning = None;
self.textbox_pad.clear();
self.minor_prompt.request_complete_repaint(ctx);
ctx.request_paint();
}
_ => {}
}
// do not process buttons when closing
if self.close_animation.is_active() {
return None;
}
self.textbox.event(ctx, event);
if let Some(Clicked) = self.confirm_btn.event(ctx, event) {
return Some(PinKeyboardMsg::Confirmed);
if animation_disabled() {
return Some(PinKeyboardMsg::Confirmed);
} else {
self.close_animation.start(ctx);
self.close_confirm = true;
}
}
if let Some(Clicked) = self.cancel_btn.event(ctx, event) {
return Some(PinKeyboardMsg::Cancelled);
if animation_disabled() {
return Some(PinKeyboardMsg::Cancelled);
} else {
self.close_animation.start(ctx);
self.close_confirm = false;
}
}
match self.erase_btn.event(ctx, event) {
Some(ButtonMsg::Clicked) => {
@ -221,8 +466,8 @@ impl Component for PinKeyboard<'_> {
_ => {}
}
for btn in &mut self.digit_btns {
if let Some(Clicked) = btn.event(ctx, event) {
if let ButtonContent::Text(text) = btn.inner().content() {
if let Some(Clicked) = btn.0.event(ctx, event) {
if let ButtonContent::Text(text) = btn.0.content() {
text.map(|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>) {
self.erase_btn.render(target);
self.textbox_pad.render(target);
let t_attach = self.attach_animation.eval();
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 let Some(ref w) = self.major_warning {
@ -249,16 +500,28 @@ impl Component for PinKeyboard<'_> {
self.major_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 {
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 {
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
let mut digits_order = ShortString::new();
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 {
text.map(|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 keyboard_overlay;
pub use unlock_overlay::UnlockOverlay;
pub use keyboard_overlay::KeyboardOverlay;
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;
#[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"))]
const IMAGE_BUFF_SIZE: usize = 2048 + ALIGN_PAD;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save