1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-24 15:28:10 +00:00

feat(core/mercury): pin entry animation

[no changelog]
This commit is contained in:
tychovrahe 2024-06-21 11:25:40 +02:00 committed by TychoVrahe
parent b99325a764
commit ff869dd864
6 changed files with 803 additions and 464 deletions

View File

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

View File

@ -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::{
button::{ component::{
Button, ButtonContent, button::{
ButtonMsg::{self, Clicked}, Button, ButtonContent,
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) {
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) { 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) { 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));

View File

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

View File

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

View File

@ -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