mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-03-03 16:56:07 +00:00
feat(eckhart): implement hold to confirm anim
- HoldToConfirmAnim is driven by the ActionBar in case the right_button is configured with `long_press` - HoldToConfirmAnim optionally draws an Header overaly with custom text - disabling animations is respected - easing function is not yet finalized - a few minor fixes along the way
This commit is contained in:
parent
c69ad3a6d5
commit
f9091f3609
@ -158,7 +158,7 @@ pub enum Stopwatch {
|
||||
}
|
||||
|
||||
impl Default for Stopwatch {
|
||||
/// Returns a new sopteed stopwatch by default.
|
||||
/// Returns a new stopped stopwatch by default.
|
||||
fn default() -> Self {
|
||||
Self::new_stopped()
|
||||
}
|
||||
|
@ -1279,7 +1279,7 @@ pub enum TranslatedString {
|
||||
haptic_feedback__enable = 885, // "Enable haptic feedback?"
|
||||
haptic_feedback__subtitle = 886, // "Setting"
|
||||
haptic_feedback__title = 887, // "Haptic feedback"
|
||||
instructions__continue_holding = 888, // "Continue\nholding"
|
||||
instructions__continue_holding = 888, // {"Bolt": "", "Caesar": "", "Delizia": "Continue\nholding", "Eckhart": "Keep holding"}
|
||||
instructions__enter_next_share = 889, // "Enter next share"
|
||||
instructions__hold_to_continue = 890, // "Hold to continue"
|
||||
instructions__hold_to_exit_tutorial = 891, // "Hold to exit tutorial"
|
||||
@ -2679,7 +2679,14 @@ impl TranslatedString {
|
||||
Self::haptic_feedback__enable => "Enable haptic feedback?",
|
||||
Self::haptic_feedback__subtitle => "Setting",
|
||||
Self::haptic_feedback__title => "Haptic feedback",
|
||||
#[cfg(feature = "layout_bolt")]
|
||||
Self::instructions__continue_holding => "",
|
||||
#[cfg(feature = "layout_caesar")]
|
||||
Self::instructions__continue_holding => "",
|
||||
#[cfg(feature = "layout_delizia")]
|
||||
Self::instructions__continue_holding => "Continue\nholding",
|
||||
#[cfg(feature = "layout_eckhart")]
|
||||
Self::instructions__continue_holding => "Keep holding",
|
||||
Self::instructions__enter_next_share => "Enter next share",
|
||||
Self::instructions__hold_to_continue => "Hold to continue",
|
||||
Self::instructions__hold_to_exit_tutorial => "Hold to exit tutorial",
|
||||
|
@ -1,13 +1,16 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Alignment2D, Offset, Rect},
|
||||
shape::{self, Renderer},
|
||||
util::Pager,
|
||||
use crate::{
|
||||
translations::TR,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::{Alignment2D, Offset, Rect},
|
||||
shape::{self, Renderer},
|
||||
util::{animation_disabled, Pager},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
button::{Button, ButtonContent, ButtonMsg},
|
||||
theme, ButtonStyleSheet,
|
||||
theme, ButtonStyleSheet, HoldToConfirmAnim,
|
||||
};
|
||||
|
||||
/// Component for control buttons in the bottom of the screen.
|
||||
@ -25,7 +28,8 @@ pub struct ActionBar {
|
||||
// Storage of original button content for paginated component
|
||||
left_original: Option<(ButtonContent, ButtonStyleSheet)>,
|
||||
right_original: Option<(ButtonContent, ButtonStyleSheet)>,
|
||||
// TODO: animation
|
||||
/// Hold to confirm animation
|
||||
htc_anim: Option<HoldToConfirmAnim>,
|
||||
}
|
||||
|
||||
pub enum ActionBarMsg {
|
||||
@ -129,6 +133,15 @@ impl ActionBar {
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
let htc_anim = right_button
|
||||
.long_press()
|
||||
.filter(|_| !animation_disabled())
|
||||
.map(|dur| {
|
||||
HoldToConfirmAnim::new()
|
||||
.with_duration(dur)
|
||||
.with_header_overlay(TR::instructions__continue_holding.into())
|
||||
});
|
||||
|
||||
Self {
|
||||
mode,
|
||||
right_button,
|
||||
@ -137,8 +150,57 @@ impl ActionBar {
|
||||
left_short: false,
|
||||
left_original,
|
||||
right_original,
|
||||
htc_anim,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle right button at the last page, this includes:
|
||||
/// - Single button mode
|
||||
/// - Double button mode at single page component
|
||||
/// - Double button mode at last page of paginated component
|
||||
/// The function takes care about triggering the correct action to
|
||||
/// HoldToConfirm or returning the correct message out of the ActionBar.
|
||||
fn right_button_at_last_page(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
msg: ButtonMsg,
|
||||
) -> Option<ActionBarMsg> {
|
||||
let is_hold = self.right_button.long_press().is_some();
|
||||
match (msg, is_hold) {
|
||||
(ButtonMsg::Pressed, true) => {
|
||||
if let Some(htc_anim) = &mut self.htc_anim {
|
||||
htc_anim.start();
|
||||
ctx.request_anim_frame();
|
||||
ctx.request_paint();
|
||||
ctx.disable_swipe();
|
||||
}
|
||||
}
|
||||
(ButtonMsg::Clicked, true) => {
|
||||
if let Some(htc_anim) = &mut self.htc_anim {
|
||||
htc_anim.stop();
|
||||
ctx.request_anim_frame();
|
||||
ctx.request_paint();
|
||||
ctx.enable_swipe();
|
||||
} else {
|
||||
// Animations disabled, return confirmed
|
||||
return Some(ActionBarMsg::Confirmed);
|
||||
}
|
||||
}
|
||||
(ButtonMsg::Released, true) => {
|
||||
if let Some(htc_anim) = &mut self.htc_anim {
|
||||
htc_anim.stop();
|
||||
ctx.request_anim_frame();
|
||||
ctx.request_paint();
|
||||
ctx.enable_swipe();
|
||||
}
|
||||
}
|
||||
(ButtonMsg::Clicked, false) | (ButtonMsg::LongPressed, true) => {
|
||||
return Some(ActionBarMsg::Confirmed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ActionBar {
|
||||
@ -172,60 +234,43 @@ impl Component for ActionBar {
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.htc_anim.event(ctx, event);
|
||||
match &self.mode {
|
||||
Mode::Single => {
|
||||
// Only handle confirm button
|
||||
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Confirmed);
|
||||
if let Some(msg) = self.right_button.event(ctx, event) {
|
||||
return self.right_button_at_last_page(ctx, msg);
|
||||
}
|
||||
}
|
||||
Mode::Double { pager } => {
|
||||
if pager.is_single() {
|
||||
// Single page - show back and confirm
|
||||
if let Some(btn) = &mut self.left_button {
|
||||
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Cancelled);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Cancelled);
|
||||
}
|
||||
if let Some(msg) = self.right_button.event(ctx, event) {
|
||||
match (&self.right_button.is_long_press(), msg) {
|
||||
(true, ButtonMsg::LongPressed) | (false, ButtonMsg::Clicked) => {
|
||||
return Some(ActionBarMsg::Confirmed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return self.right_button_at_last_page(ctx, msg);
|
||||
}
|
||||
} else if pager.is_first() && !pager.is_single() {
|
||||
// First page of multiple - go back and next page
|
||||
if let Some(btn) = &mut self.left_button {
|
||||
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Cancelled);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Cancelled);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Next);
|
||||
}
|
||||
} else if pager.is_last() && !pager.is_single() {
|
||||
// Last page - enable up button, show confirm
|
||||
if let Some(btn) = &mut self.left_button {
|
||||
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
if let Some(msg) = self.right_button.event(ctx, event) {
|
||||
match (&self.right_button.is_long_press(), msg) {
|
||||
(true, ButtonMsg::LongPressed) | (false, ButtonMsg::Clicked) => {
|
||||
return Some(ActionBarMsg::Confirmed);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return self.right_button_at_last_page(ctx, msg);
|
||||
}
|
||||
} else {
|
||||
// Middle pages - navigations up/down
|
||||
if let Some(btn) = &mut self.left_button {
|
||||
if let Some(ButtonMsg::Clicked) = btn.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.left_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Prev);
|
||||
}
|
||||
if let Some(ButtonMsg::Clicked) = self.right_button.event(ctx, event) {
|
||||
return Some(ActionBarMsg::Next);
|
||||
@ -246,8 +291,12 @@ impl Component for ActionBar {
|
||||
btn.render(target);
|
||||
}
|
||||
self.right_button.render(target);
|
||||
if let Some(htc_anim) = &self.htc_anim {
|
||||
htc_anim.render(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for ActionBar {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
|
@ -135,8 +135,8 @@ impl Button {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_long_press(&self) -> bool {
|
||||
self.long_press.is_some()
|
||||
pub fn long_press(&self) -> Option<Duration> {
|
||||
self.long_press
|
||||
}
|
||||
|
||||
pub fn is_disabled(&self) -> bool {
|
||||
|
@ -171,7 +171,8 @@ impl Header {
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Calculates the width needed for the left icon, be it a button with icon or just icon
|
||||
/// Calculates the width needed for the left icon, be it a button with icon
|
||||
/// or just icon
|
||||
fn left_icon_width(&self) -> i16 {
|
||||
let margin_right: i16 = 16; // [px]
|
||||
if let Some(b) = &self.left_button {
|
||||
|
@ -245,7 +245,6 @@ impl<'a> Instruction<'a> {
|
||||
fn height(&self) -> i16 {
|
||||
let text_area_width = screen().inset(Hint::HINT_INSETS).width() - self.icon_width();
|
||||
let calculated_height = self.label.text_height(text_area_width);
|
||||
dbg_print!("Instruction height: {}\n", calculated_height as i16);
|
||||
debug_assert!(calculated_height <= Hint::HEIGHT_MAXIMAL);
|
||||
calculated_height
|
||||
}
|
||||
|
@ -0,0 +1,128 @@
|
||||
use crate::{
|
||||
strutil::TString,
|
||||
time::{Duration, Stopwatch},
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx, Never},
|
||||
display::Color,
|
||||
geometry::{Offset, Rect},
|
||||
layout_eckhart::{cshape::ScreenBorder, fonts},
|
||||
shape::{self, Renderer},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{constant, theme, Header};
|
||||
|
||||
/// A component that displays a border that grows from the bottom of the screen
|
||||
/// to the top. The animation is parametrizable by color and duration.
|
||||
pub struct HoldToConfirmAnim {
|
||||
/// Duration of the animation
|
||||
duration: Duration,
|
||||
/// Screen border and header overlay color
|
||||
color: Color,
|
||||
/// Screen border shape
|
||||
border: ScreenBorder,
|
||||
/// Timer for the animation
|
||||
timer: Stopwatch,
|
||||
/// Header overlay text shown during the animation
|
||||
header_overlay: Option<TString<'static>>,
|
||||
}
|
||||
|
||||
impl HoldToConfirmAnim {
|
||||
pub fn new() -> Self {
|
||||
let default_color = theme::GREEN_LIME;
|
||||
Self {
|
||||
duration: theme::CONFIRM_HOLD_DURATION,
|
||||
color: default_color,
|
||||
border: ScreenBorder::new(default_color),
|
||||
timer: Stopwatch::default(),
|
||||
header_overlay: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_color(mut self, color: Color) -> Self {
|
||||
self.color = color;
|
||||
self.border = ScreenBorder::new(color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_duration(mut self, duration: Duration) -> Self {
|
||||
self.duration = duration;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_header_overlay(mut self, header_text: TString<'static>) -> Self {
|
||||
self.header_overlay = Some(header_text);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
self.timer = Stopwatch::new_started();
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.timer = Stopwatch::new_stopped();
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
self.timer.is_running_within(self.duration)
|
||||
}
|
||||
|
||||
fn get_clip(&self) -> Rect {
|
||||
// TODO:
|
||||
// 1) there will be some easer function
|
||||
// 2) the growth of the top bar cannot be done with just one clip
|
||||
let screen = constant::screen();
|
||||
let ratio = self.timer.elapsed() / self.duration;
|
||||
let clip_height = ((ratio * screen.height() as f32) as i16).clamp(0, screen.height());
|
||||
Rect::from_bottom_left_and_size(
|
||||
screen.bottom_left(),
|
||||
Offset::new(screen.width(), clip_height),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for HoldToConfirmAnim {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Event::Timer(EventCtx::ANIM_FRAME_TIMER) = event {
|
||||
if self.is_active() {
|
||||
ctx.request_anim_frame();
|
||||
ctx.request_paint();
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
|
||||
if self.is_active() {
|
||||
// override header with custom text
|
||||
if let Some(text) = self.header_overlay {
|
||||
let font = fonts::FONT_SATOSHI_REGULAR_22;
|
||||
let header_pad = Rect::from_top_left_and_size(
|
||||
constant::screen().top_left(),
|
||||
Offset::new(constant::screen().width(), Header::HEADER_HEIGHT),
|
||||
);
|
||||
shape::Bar::new(header_pad)
|
||||
.with_bg(theme::BG)
|
||||
.render(target);
|
||||
text.map(|text| {
|
||||
let text_pos = header_pad.top_left()
|
||||
+ Offset::new(24, font.vert_center(0, Header::HEADER_HEIGHT, text));
|
||||
shape::Text::new(text_pos, text, font)
|
||||
.with_fg(self.color)
|
||||
.render(target);
|
||||
});
|
||||
}
|
||||
// growing border
|
||||
let clip = self.get_clip();
|
||||
target.in_clip(clip, &|target| {
|
||||
self.border.render(target);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ mod button;
|
||||
mod error;
|
||||
mod header;
|
||||
mod hint;
|
||||
mod hold_to_confirm;
|
||||
mod result;
|
||||
mod text_screen;
|
||||
mod vertical_menu;
|
||||
@ -15,6 +16,7 @@ pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet
|
||||
pub use error::ErrorScreen;
|
||||
pub use header::{Header, HeaderMsg};
|
||||
pub use hint::Hint;
|
||||
pub use hold_to_confirm::HoldToConfirmAnim;
|
||||
pub use result::{ResultFooter, ResultScreen, ResultStyle};
|
||||
pub use text_screen::{AllowedTextContent, TextScreen, TextScreenMsg};
|
||||
pub use vertical_menu::{VerticalMenu, VerticalMenuMsg, MENU_MAX_ITEMS};
|
||||
|
@ -19,7 +19,8 @@ impl ScreenBorder {
|
||||
pub fn new(color: Color) -> Self {
|
||||
let screen = constant::screen();
|
||||
|
||||
// Top bar: from the right edge of top-left icon to the left edge of top-right icon.
|
||||
// Top bar: from the right edge of top-left icon to the left edge of top-right
|
||||
// icon.
|
||||
let top_bar_rect = Rect {
|
||||
x0: screen.x0 + ICON_BORDER_TL.toif.width(),
|
||||
y0: screen.y0,
|
||||
@ -27,7 +28,8 @@ impl ScreenBorder {
|
||||
y1: screen.y0 + 2,
|
||||
};
|
||||
|
||||
// Bottom bar: from the right edge of bottom-left icon to the left edge of bottom-right icon.
|
||||
// Bottom bar: from the right edge of bottom-left icon to the left edge of
|
||||
// bottom-right icon.
|
||||
let bottom_bar_rect = Rect {
|
||||
x0: screen.x0 + ICON_BORDER_BL.toif.width(),
|
||||
y0: screen.y1 - 2,
|
||||
@ -35,14 +37,16 @@ impl ScreenBorder {
|
||||
y1: screen.y1,
|
||||
};
|
||||
|
||||
// Left bar: from the bottom edge of top-left icon to the top edge of bottom-left icon.
|
||||
// Left bar: from the bottom edge of top-left icon to the top edge of
|
||||
// bottom-left icon.
|
||||
let left_bar_rect = Rect {
|
||||
x0: screen.x0,
|
||||
y0: screen.y0 + ICON_BORDER_TL.toif.height() - 1,
|
||||
x1: screen.x0 + 2,
|
||||
y1: screen.y1 - ICON_BORDER_BL.toif.height(),
|
||||
};
|
||||
// Right bar: from the bottom edge of top-right icon to the top edge of bottom-right icon.
|
||||
// Right bar: from the bottom edge of top-right icon to the top edge of
|
||||
// bottom-right icon.
|
||||
let right_bar_rect = Rect {
|
||||
x0: screen.x1 - 2,
|
||||
y0: screen.y0 + ICON_BORDER_TR.toif.height() - 1,
|
||||
|
@ -634,7 +634,7 @@ def show_success(
|
||||
title: str,
|
||||
button: str,
|
||||
description: str = "",
|
||||
allow_cancel: bool = True,
|
||||
allow_cancel: bool = False,
|
||||
time_ms: int = 0,
|
||||
) -> LayoutObj[UiResult]:
|
||||
"""Success modal. No buttons shown when `button` is empty string."""
|
||||
|
@ -386,7 +386,7 @@ class TR:
|
||||
inputs__return: str = "RETURN"
|
||||
inputs__show: str = "SHOW"
|
||||
inputs__space: str = "SPACE"
|
||||
instructions__continue_holding: str = "Continue\nholding"
|
||||
instructions__continue_holding: str = ""
|
||||
instructions__continue_in_app: str = "Continue in the app"
|
||||
instructions__enter_next_share: str = "Enter next share"
|
||||
instructions__hold_to_confirm: str = "Hold to confirm"
|
||||
|
@ -388,7 +388,7 @@
|
||||
"inputs__return": "RETURN",
|
||||
"inputs__show": "SHOW",
|
||||
"inputs__space": "SPACE",
|
||||
"instructions__continue_holding": "Continue\nholding",
|
||||
"instructions__continue_holding": {"Bolt": "", "Caesar": "", "Delizia": "Continue\nholding", "Eckhart": "Keep holding"},
|
||||
"instructions__continue_in_app": "Continue in the app",
|
||||
"instructions__enter_next_share": "Enter next share",
|
||||
"instructions__hold_to_confirm": "Hold to confirm",
|
||||
|
Loading…
Reference in New Issue
Block a user