diff --git a/core/embed/rust/src/ui/model_mercury/component/mod.rs b/core/embed/rust/src/ui/model_mercury/component/mod.rs index 3e2285b2e3..5c0575efdb 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -26,6 +26,7 @@ pub mod number_input_slider; #[cfg(feature = "translations")] mod page; mod progress; +#[cfg(feature = "translations")] mod prompt_screen; mod result; mod scroll; @@ -36,6 +37,8 @@ mod share_words; mod simple_page; mod status_screen; mod swipe_up_screen; +#[cfg(feature = "translations")] +mod tap_to_confirm; mod welcome_screen; #[cfg(feature = "translations")] @@ -72,6 +75,7 @@ pub use number_input_slider::NumberInputSliderDialog; #[cfg(feature = "translations")] pub use page::ButtonPage; pub use progress::Progress; +#[cfg(feature = "translations")] pub use prompt_screen::PromptScreen; pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use scroll::ScrollBar; @@ -82,6 +86,8 @@ pub use share_words::ShareWords; pub use simple_page::SimplePage; pub use status_screen::StatusScreen; pub use swipe_up_screen::{SwipeUpScreen, SwipeUpScreenMsg}; +#[cfg(feature = "translations")] +pub use tap_to_confirm::TapToConfirm; pub use vertical_menu::{VerticalMenu, VerticalMenuChoiceMsg}; pub use welcome_screen::WelcomeScreen; diff --git a/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs b/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs index fcbd39ca37..c330100d94 100644 --- a/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs +++ b/core/embed/rust/src/ui/model_mercury/component/prompt_screen.rs @@ -1,78 +1,41 @@ -use crate::{ - time::Duration, - ui::{ - component::{Component, Event, EventCtx}, - display::Color, - geometry::{Alignment2D, Offset, Rect}, - shape, - shape::Renderer, - util::animation_disabled, - }, +use crate::ui::{ + component::{Component, Event, EventCtx}, + geometry::Rect, + model_mercury::theme, + shape::Renderer, }; -use super::{theme, Button, ButtonContent, ButtonMsg}; - -const HOLD_DURATION_MS: u32 = 1000; -const BUTTON_SIZE: i16 = 110; +use super::{HoldToConfirm, TapToConfirm}; /// 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". #[derive(Clone)] -pub struct PromptScreen { - area: Rect, - button: Button, - circle_color: Color, - circle_pad_color: Color, - circle_inner_color: Color, - dismiss_type: DismissType, -} - -#[derive(Clone)] -enum DismissType { - Tap, - Hold, +pub enum PromptScreen { + Tap(TapToConfirm), + Hold(HoldToConfirm), } impl PromptScreen { pub fn new_hold_to_confirm() -> Self { - let icon = theme::ICON_SIGN; - 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, - } + PromptScreen::Hold(HoldToConfirm::new()) } pub fn new_tap_to_confirm() -> Self { - let icon = theme::ICON_SIMPLE_CHECKMARK; - let button = Button::new(ButtonContent::Icon(icon)).styled(theme::button_default()); - Self { - area: Rect::zero(), - circle_color: theme::GREEN, - circle_inner_color: theme::GREEN, - circle_pad_color: theme::GREY_EXTRA_DARK, - dismiss_type: DismissType::Tap, - button, - } + PromptScreen::Tap(TapToConfirm::new( + theme::GREEN, + theme::GREEN, + theme::GREY_EXTRA_DARK, + theme::GREEN_LIGHT, + )) } pub fn new_tap_to_cancel() -> Self { - let icon = theme::ICON_SIMPLE_CHECKMARK; - let button = Button::new(ButtonContent::Icon(icon)).styled(theme::button_default()); - Self { - area: Rect::zero(), - circle_color: theme::ORANGE_LIGHT, - circle_inner_color: theme::ORANGE_LIGHT, - circle_pad_color: theme::GREY_EXTRA_DARK, - dismiss_type: DismissType::Tap, - button, - } + PromptScreen::Tap(TapToConfirm::new( + theme::ORANGE_LIGHT, + theme::ORANGE_LIGHT, + theme::GREY_EXTRA_DARK, + theme::ORANGE_DIMMED, + )) } } @@ -80,30 +43,17 @@ impl Component for PromptScreen { type Msg = (); fn place(&mut self, bounds: Rect) -> Rect { - self.area = bounds; - self.button.place(Rect::snap( - self.area.center(), - Offset::uniform(BUTTON_SIZE), - Alignment2D::CENTER, - )); - bounds + match self { + PromptScreen::Tap(t) => t.place(bounds), + PromptScreen::Hold(h) => h.place(bounds), + } } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - let btn_msg = self.button.event(ctx, event); - match (&self.dismiss_type, btn_msg) { - (DismissType::Tap, Some(ButtonMsg::Clicked)) => { - return Some(()); - } - (DismissType::Hold, Some(ButtonMsg::LongPressed)) => { - return Some(()); - } - (DismissType::Hold, Some(ButtonMsg::Clicked)) if animation_disabled() => { - return Some(()); - } - _ => (), + match self { + PromptScreen::Tap(t) => t.event(ctx, event), + PromptScreen::Hold(h) => h.event(ctx, event), } - None } fn paint(&mut self) { @@ -111,30 +61,10 @@ impl Component for PromptScreen { } fn render<'s>(&self, target: &mut impl Renderer<'s>) { - shape::Circle::new(self.area.center(), 70) - .with_fg(self.circle_pad_color) - .with_bg(theme::BLACK) - .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); + match self { + PromptScreen::Tap(t) => t.render(target), + PromptScreen::Hold(h) => h.render(target), + } } } @@ -145,6 +75,9 @@ impl crate::ui::flow::Swipable<()> for PromptScreen {} impl crate::trace::Trace for PromptScreen { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.component("PromptScreen"); - t.child("button", &self.button); + match self { + PromptScreen::Tap(c) => t.child("TapToConfirm", c), + PromptScreen::Hold(c) => t.child("HoldToConfirm", c), + } } } diff --git a/core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs b/core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs new file mode 100644 index 0000000000..237118d22e --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs @@ -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 { + 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); + } +}