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:
parent
491aeaa0f5
commit
c8c7001301
@ -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;
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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