mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-22 22:38:08 +00:00
feat(core): implement tap to confirm in mercury UI
[no changelog]
This commit is contained in:
parent
491aeaa0f5
commit
c8c7001301
@ -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;
|
||||
|
||||
|
@ -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<Self::Msg> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
255
core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs
Normal file
255
core/embed/rust/src/ui/model_mercury/component/tap_to_confirm.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user