diff --git a/core/embed/rust/src/time.rs b/core/embed/rust/src/time.rs index 98a5961251..2f38f53b25 100644 --- a/core/embed/rust/src/time.rs +++ b/core/embed/rust/src/time.rs @@ -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() } diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs index dbf3ebdfce..903c06b36d 100644 --- a/core/embed/rust/src/translations/generated/translated_string.rs +++ b/core/embed/rust/src/translations/generated/translated_string.rs @@ -1255,7 +1255,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" diff --git a/core/embed/rust/src/ui/layout_eckhart/component/action_bar.rs b/core/embed/rust/src/ui/layout_eckhart/component/action_bar.rs index 9909c5f567..212f243aaa 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/action_bar.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/action_bar.rs @@ -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, } 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 { + 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.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) { diff --git a/core/embed/rust/src/ui/layout_eckhart/component/button.rs b/core/embed/rust/src/ui/layout_eckhart/component/button.rs index 6116d23d72..7f68c24ea1 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/button.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/button.rs @@ -135,8 +135,8 @@ impl Button { ) } - pub fn is_long_press(&self) -> bool { - self.long_press.is_some() + pub fn long_press(&self) -> Option { + self.long_press } pub fn is_disabled(&self) -> bool { diff --git a/core/embed/rust/src/ui/layout_eckhart/component/header.rs b/core/embed/rust/src/ui/layout_eckhart/component/header.rs index bfaf5d0f00..dd53e9f25d 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/header.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/header.rs @@ -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 { diff --git a/core/embed/rust/src/ui/layout_eckhart/component/hint.rs b/core/embed/rust/src/ui/layout_eckhart/component/hint.rs index a0896e7239..dfbcd5dc16 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/hint.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/hint.rs @@ -244,7 +244,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 } diff --git a/core/embed/rust/src/ui/layout_eckhart/component/hold_to_confirm.rs b/core/embed/rust/src/ui/layout_eckhart/component/hold_to_confirm.rs new file mode 100644 index 0000000000..b25a1dc949 --- /dev/null +++ b/core/embed/rust/src/ui/layout_eckhart/component/hold_to_confirm.rs @@ -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>, +} + +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 { + 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); + }); + } + } +} diff --git a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs index 5d1764cb94..2407b01f59 100644 --- a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs +++ b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs @@ -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}; diff --git a/core/embed/rust/src/ui/layout_eckhart/cshape/screen_border.rs b/core/embed/rust/src/ui/layout_eckhart/cshape/screen_border.rs index 910d258636..1f76d3675e 100644 --- a/core/embed/rust/src/ui/layout_eckhart/cshape/screen_border.rs +++ b/core/embed/rust/src/ui/layout_eckhart/cshape/screen_border.rs @@ -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, diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Pattern_Idle.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Pattern_Idle.jpg new file mode 100644 index 0000000000..3672c98150 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Pattern_Idle.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Pattern_LED.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Pattern_LED.jpg new file mode 100644 index 0000000000..40a40e6c65 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Pattern_LED.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_Idle.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_Idle.jpg new file mode 100644 index 0000000000..9ba98c6e8e Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_Idle.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED.jpg new file mode 100644 index 0000000000..b29541b720 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g.jpg new file mode 100644 index 0000000000..adc68abccb Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g2.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g2.jpg new file mode 100644 index 0000000000..1724ae5563 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g2.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g3.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g3.jpg new file mode 100644 index 0000000000..c70eef01a3 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g3.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g3_gimp.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g3_gimp.jpg new file mode 100644 index 0000000000..70d2970d41 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g3_gimp.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g4.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g4.jpg new file mode 100644 index 0000000000..89c6369120 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/BG_Simple_LED_CropD-g4.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/Light_Compressed.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/Light_Compressed.jpg new file mode 100644 index 0000000000..1e70444bdf Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/Light_Compressed.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/Light_check.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/Light_check.jpg new file mode 100644 index 0000000000..96e9d5fbc1 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/Light_check.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/led_light.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/led_light.jpg new file mode 100644 index 0000000000..d6f75c7ce9 Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/led_light.jpg differ diff --git a/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/led_light_gs.jpg b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/led_light_gs.jpg new file mode 100644 index 0000000000..22a9a26bbe Binary files /dev/null and b/core/embed/rust/src/ui/layout_eckhart/res/defaut_homescreen/led_light_gs.jpg differ diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi index 0c9e8fae23..111e50d7c5 100644 --- a/core/mocks/generated/trezorui_api.pyi +++ b/core/mocks/generated/trezorui_api.pyi @@ -639,7 +639,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.""" diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi index 62ac281ed1..0d3908ad7d 100644 --- a/core/mocks/trezortranslate_keys.pyi +++ b/core/mocks/trezortranslate_keys.pyi @@ -387,7 +387,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" diff --git a/core/translations/en.json b/core/translations/en.json index 9fa845f024..b04729b378 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -389,7 +389,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",