1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-12-23 06:48:16 +00:00

feat(core): implement tap to confirm in mercury UI

[no changelog]
This commit is contained in:
tychovrahe 2024-05-08 14:56:19 +02:00 committed by TychoVrahe
parent 491aeaa0f5
commit c8c7001301
3 changed files with 298 additions and 104 deletions

View File

@ -26,6 +26,7 @@ pub mod number_input_slider;
#[cfg(feature = "translations")] #[cfg(feature = "translations")]
mod page; mod page;
mod progress; mod progress;
#[cfg(feature = "translations")]
mod prompt_screen; mod prompt_screen;
mod result; mod result;
mod scroll; mod scroll;
@ -36,6 +37,8 @@ mod share_words;
mod simple_page; mod simple_page;
mod status_screen; mod status_screen;
mod swipe_up_screen; mod swipe_up_screen;
#[cfg(feature = "translations")]
mod tap_to_confirm;
mod welcome_screen; mod welcome_screen;
#[cfg(feature = "translations")] #[cfg(feature = "translations")]
@ -72,6 +75,7 @@ pub use number_input_slider::NumberInputSliderDialog;
#[cfg(feature = "translations")] #[cfg(feature = "translations")]
pub use page::ButtonPage; pub use page::ButtonPage;
pub use progress::Progress; pub use progress::Progress;
#[cfg(feature = "translations")]
pub use prompt_screen::PromptScreen; pub use prompt_screen::PromptScreen;
pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use result::{ResultFooter, ResultScreen, ResultStyle};
pub use scroll::ScrollBar; pub use scroll::ScrollBar;
@ -82,6 +86,8 @@ pub use share_words::ShareWords;
pub use simple_page::SimplePage; pub use simple_page::SimplePage;
pub use status_screen::StatusScreen; pub use status_screen::StatusScreen;
pub use swipe_up_screen::{SwipeUpScreen, SwipeUpScreenMsg}; pub use swipe_up_screen::{SwipeUpScreen, SwipeUpScreenMsg};
#[cfg(feature = "translations")]
pub use tap_to_confirm::TapToConfirm;
pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg}; pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg};
pub use welcome_screen::WelcomeScreen; pub use welcome_screen::WelcomeScreen;

View File

@ -1,78 +1,41 @@
use crate::{ use crate::ui::{
time::Duration, component::{Component, Event, EventCtx},
ui::{ geometry::Rect,
component::{Component, Event, EventCtx}, model_mercury::theme,
display::Color, shape::Renderer,
geometry::{Alignment2D, Offset, Rect},
shape,
shape::Renderer,
util::animation_disabled,
},
}; };
use super::{theme, Button, ButtonContent, ButtonMsg}; use super::{HoldToConfirm, TapToConfirm};
const HOLD_DURATION_MS: u32 = 1000;
const BUTTON_SIZE: i16 = 110;
/// Component requesting an action from a user. Most typically embedded as a /// Component requesting an action from a user. Most typically embedded as a
/// content of a Frame and promptin "Tap to confirm" or "Hold to XYZ". /// content of a Frame and promptin "Tap to confirm" or "Hold to XYZ".
#[derive(Clone)] #[derive(Clone)]
pub struct PromptScreen { pub enum PromptScreen {
area: Rect, Tap(TapToConfirm),
button: Button, Hold(HoldToConfirm),
circle_color: Color,
circle_pad_color: Color,
circle_inner_color: Color,
dismiss_type: DismissType,
}
#[derive(Clone)]
enum DismissType {
Tap,
Hold,
} }
impl PromptScreen { impl PromptScreen {
pub fn new_hold_to_confirm() -> Self { pub fn new_hold_to_confirm() -> Self {
let icon = theme::ICON_SIGN; PromptScreen::Hold(HoldToConfirm::new())
let button = Button::new(ButtonContent::Icon(icon))
.styled(theme::button_default())
.with_long_press(Duration::from_millis(HOLD_DURATION_MS));
Self {
area: Rect::zero(),
circle_color: theme::GREEN,
circle_pad_color: theme::GREY_EXTRA_DARK,
circle_inner_color: theme::GREEN_LIGHT,
dismiss_type: DismissType::Hold,
button,
}
} }
pub fn new_tap_to_confirm() -> Self { pub fn new_tap_to_confirm() -> Self {
let icon = theme::ICON_SIMPLE_CHECKMARK; PromptScreen::Tap(TapToConfirm::new(
let button = Button::new(ButtonContent::Icon(icon)).styled(theme::button_default()); theme::GREEN,
Self { theme::GREEN,
area: Rect::zero(), theme::GREY_EXTRA_DARK,
circle_color: theme::GREEN, theme::GREEN_LIGHT,
circle_inner_color: theme::GREEN, ))
circle_pad_color: theme::GREY_EXTRA_DARK,
dismiss_type: DismissType::Tap,
button,
}
} }
pub fn new_tap_to_cancel() -> Self { pub fn new_tap_to_cancel() -> Self {
let icon = theme::ICON_SIMPLE_CHECKMARK; PromptScreen::Tap(TapToConfirm::new(
let button = Button::new(ButtonContent::Icon(icon)).styled(theme::button_default()); theme::ORANGE_LIGHT,
Self { theme::ORANGE_LIGHT,
area: Rect::zero(), theme::GREY_EXTRA_DARK,
circle_color: theme::ORANGE_LIGHT, theme::ORANGE_DIMMED,
circle_inner_color: theme::ORANGE_LIGHT, ))
circle_pad_color: theme::GREY_EXTRA_DARK,
dismiss_type: DismissType::Tap,
button,
}
} }
} }
@ -80,30 +43,17 @@ impl Component for PromptScreen {
type Msg = (); type Msg = ();
fn place(&mut self, bounds: Rect) -> Rect { fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds; match self {
self.button.place(Rect::snap( PromptScreen::Tap(t) => t.place(bounds),
self.area.center(), PromptScreen::Hold(h) => h.place(bounds),
Offset::uniform(BUTTON_SIZE), }
Alignment2D::CENTER,
));
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> {
let btn_msg = self.button.event(ctx, event); match self {
match (&self.dismiss_type, btn_msg) { PromptScreen::Tap(t) => t.event(ctx, event),
(DismissType::Tap, Some(ButtonMsg::Clicked)) => { PromptScreen::Hold(h) => h.event(ctx, event),
return Some(());
}
(DismissType::Hold, Some(ButtonMsg::LongPressed)) => {
return Some(());
}
(DismissType::Hold, Some(ButtonMsg::Clicked)) if animation_disabled() => {
return Some(());
}
_ => (),
} }
None
} }
fn paint(&mut self) { fn paint(&mut self) {
@ -111,30 +61,10 @@ impl Component for PromptScreen {
} }
fn render<'s>(&self, target: &mut impl Renderer<'s>) { fn render<'s>(&self, target: &mut impl Renderer<'s>) {
shape::Circle::new(self.area.center(), 70) match self {
.with_fg(self.circle_pad_color) PromptScreen::Tap(t) => t.render(target),
.with_bg(theme::BLACK) PromptScreen::Hold(h) => h.render(target),
.with_thickness(20) }
.render(target);
shape::Circle::new(self.area.center(), 50)
.with_fg(self.circle_color)
.with_bg(theme::BLACK)
.with_thickness(2)
.render(target);
shape::Circle::new(self.area.center(), 48)
.with_fg(self.circle_pad_color)
.with_bg(theme::BLACK)
.with_thickness(8)
.render(target);
matches!(self.dismiss_type, DismissType::Hold).then(|| {
shape::Circle::new(self.area.center(), 40)
.with_fg(self.circle_inner_color)
.with_bg(theme::BLACK)
.with_thickness(2)
.render(target);
});
self.button
.render_content(target, self.button.style(), 0xff);
} }
} }
@ -145,6 +75,9 @@ impl crate::ui::flow::Swipable<()> for PromptScreen {}
impl crate::trace::Trace for PromptScreen { impl crate::trace::Trace for PromptScreen {
fn trace(&self, t: &mut dyn crate::trace::Tracer) { fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("PromptScreen"); t.component("PromptScreen");
t.child("button", &self.button); match self {
PromptScreen::Tap(c) => t.child("TapToConfirm", c),
PromptScreen::Hold(c) => t.child("HoldToConfirm", c),
}
} }
} }

View File

@ -0,0 +1,255 @@
use crate::{
time::Duration,
ui::{
component::{Component, Event, EventCtx},
display::Color,
geometry::{Alignment2D, Offset, Rect},
lerp::Lerp,
shape,
shape::Renderer,
util::animation_disabled,
},
};
use super::{theme, Button, ButtonContent, ButtonMsg};
use crate::{time::Stopwatch, ui::constant::screen};
use pareen;
#[derive(Default, Clone)]
struct TapToConfirmAmin {
pub timer: Stopwatch,
}
impl TapToConfirmAmin {
const DURATION_MS: u32 = 600;
pub fn is_active(&self) -> bool {
self.timer
.is_running_within(Duration::from_millis(Self::DURATION_MS))
}
pub fn is_finished(&self) -> bool {
self.timer.elapsed() >= Duration::from_millis(Self::DURATION_MS)
}
pub fn eval(&self) -> f32 {
if animation_disabled() {
return 0.0;
}
self.timer.elapsed().to_millis() as f32 / 1000.0
}
pub fn get_parent_cover_opacity(&self, t: f32) -> u8 {
let parent_cover_opacity = pareen::constant(0.0).seq_ease_in_out(
0.0,
easer::functions::Cubic,
0.2,
pareen::constant(1.0),
);
u8::lerp(0, 255, parent_cover_opacity.eval(t))
}
pub fn get_circle_scale(&self, t: f32) -> i16 {
let circle_scale = pareen::constant(0.0).seq_ease_in_out(
0.0,
easer::functions::Cubic,
0.58,
pareen::constant(1.0),
);
i16::lerp(0, 80, circle_scale.eval(t))
}
pub fn get_circle_color(&self, t: f32, final_color: Color) -> Color {
let circle_color = pareen::constant(0.0).seq_ease_in_out(
0.0,
easer::functions::Cubic,
0.55,
pareen::constant(1.0),
);
Color::lerp(Color::black(), final_color, circle_color.eval(t))
}
pub fn get_circle_opacity(&self, t: f32) -> u8 {
let circle_opacity = pareen::constant(0.0).seq_ease_in_out(
0.2,
easer::functions::Cubic,
0.8,
pareen::constant(1.0),
);
u8::lerp(255, 0, circle_opacity.eval(t))
}
pub fn get_pad_opacity(&self, t: f32) -> u8 {
let pad_opacity = pareen::constant(0.0).seq_ease_in_out(
0.2,
easer::functions::Cubic,
0.4,
pareen::constant(1.0),
);
u8::lerp(255, 0, pad_opacity.eval(t))
}
pub fn get_black_mask_scale(&self, t: f32) -> i16 {
let black_mask_scale = pareen::constant(0.0).seq_ease_in_out(
0.2,
easer::functions::Cubic,
0.8,
pareen::constant(1.0),
);
i16::lerp(0, 400, black_mask_scale.eval(t))
}
pub fn start(&mut self) {
self.timer.start();
}
pub fn reset(&mut self) {
self.timer = Stopwatch::new_stopped();
}
}
/// Component requesting a Tap to confirm action from a user. Most typically
/// embedded as a content of a Frame.
#[derive(Clone)]
pub struct TapToConfirm {
area: Rect,
button: Button,
circle_color: Color,
circle_pad_color: Color,
circle_inner_color: Color,
mask_color: Color,
anim: TapToConfirmAmin,
}
#[derive(Clone)]
enum DismissType {
Tap,
Hold,
}
impl TapToConfirm {
pub fn new(
circle_color: Color,
circle_inner_color: Color,
circle_pad_color: Color,
mask_color: Color,
) -> Self {
let button = Button::new(ButtonContent::Empty).styled(theme::button_default());
Self {
area: Rect::zero(),
circle_color,
circle_inner_color,
circle_pad_color,
mask_color,
button,
anim: TapToConfirmAmin::default(),
}
}
}
impl Component for TapToConfirm {
type Msg = ();
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
self.button.place(Rect::snap(
self.area.center(),
Offset::uniform(80),
Alignment2D::CENTER,
));
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
let btn_msg = self.button.event(ctx, event);
match btn_msg {
Some(ButtonMsg::Pressed) => {
self.anim.start();
ctx.request_anim_frame();
ctx.request_paint();
}
Some(ButtonMsg::Released) => {
self.anim.reset();
ctx.request_anim_frame();
ctx.request_paint();
}
Some(ButtonMsg::Clicked) => {
if animation_disabled() {
return Some(());
}
}
_ => (),
}
if self.anim.is_active() {
ctx.request_anim_frame();
ctx.request_paint();
}
if self.anim.is_finished() {
return Some(());
};
None
}
fn paint(&mut self) {
unimplemented!()
}
fn render<'s>(&self, target: &mut impl Renderer<'s>) {
const PAD_RADIUS: i16 = 70;
const PAD_THICKNESS: i16 = 20;
const CIRCLE_RADIUS: i16 = 50;
const INNER_CIRCLE_RADIUS: i16 = 40;
const CIRCLE_THICKNESS: i16 = 2;
let t = self.anim.eval();
let center = self.area.center();
shape::Bar::new(screen())
.with_fg(theme::BLACK)
.with_bg(theme::BLACK)
.with_alpha(self.anim.get_parent_cover_opacity(t))
.render(target);
shape::Circle::new(center, PAD_RADIUS)
.with_fg(self.circle_pad_color)
.with_bg(theme::BLACK)
.with_thickness(PAD_THICKNESS)
.with_alpha(self.anim.get_pad_opacity(t))
.render(target);
shape::Circle::new(center, CIRCLE_RADIUS)
.with_fg(self.circle_color)
.with_bg(theme::BLACK)
.with_thickness(CIRCLE_THICKNESS)
.render(target);
shape::Circle::new(center, CIRCLE_RADIUS - CIRCLE_THICKNESS)
.with_fg(self.circle_pad_color)
.with_bg(theme::BLACK)
.with_thickness(CIRCLE_RADIUS - CIRCLE_THICKNESS - INNER_CIRCLE_RADIUS)
.render(target);
shape::Circle::new(center, self.anim.get_circle_scale(t))
.with_fg(self.anim.get_circle_color(t, self.mask_color))
.with_alpha(self.anim.get_circle_opacity(t))
.render(target);
shape::Circle::new(center, self.anim.get_black_mask_scale(t))
.with_fg(theme::BLACK)
.render(target);
shape::ToifImage::new(center, theme::ICON_SIMPLE_CHECKMARK.toif)
.with_fg(theme::GREY)
.with_alpha(255 - self.anim.get_parent_cover_opacity(t))
.with_align(Alignment2D::CENTER)
.render(target);
}
}
#[cfg(feature = "micropython")]
impl crate::ui::flow::Swipable<()> for TapToConfirm {}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for TapToConfirm {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("StatusScreen");
t.child("button", &self.button);
}
}