From 67fe334dc292fc0bc9233cee2e252ae3eb5ec5a8 Mon Sep 17 00:00:00 2001 From: tychovrahe Date: Sun, 5 May 2024 14:06:31 +0200 Subject: [PATCH] feat(core): add hold-to-confirm animation to mercury UI [no changelog] --- core/embed/rust/src/time.rs | 15 +- core/embed/rust/src/ui/component/label.rs | 5 + .../rust/src/ui/component/text/layout.rs | 48 ++- .../src/ui/model_mercury/component/button.rs | 57 ++- .../src/ui/model_mercury/component/frame.rs | 16 +- .../component/hold_to_confirm.rs | 331 ++++++++++++++++++ .../src/ui/model_mercury/component/mod.rs | 5 + .../model_mercury/component/prompt_screen.rs | 3 +- .../flow/confirm_reset_create.rs | 29 +- .../rust/src/ui/model_mercury/theme/mod.rs | 1 + core/translations/en.json | 2 + 11 files changed, 460 insertions(+), 52 deletions(-) create mode 100644 core/embed/rust/src/ui/model_mercury/component/hold_to_confirm.rs diff --git a/core/embed/rust/src/time.rs b/core/embed/rust/src/time.rs index 40505691f1..72eb7455c6 100644 --- a/core/embed/rust/src/time.rs +++ b/core/embed/rust/src/time.rs @@ -67,18 +67,6 @@ impl Div for Duration { } } -impl From for Duration { - fn from(value: f32) -> Self { - Self::from_millis((value * 1000.0) as u32) - } -} - -impl From for f32 { - fn from(value: Duration) -> Self { - value.to_millis() as f32 / 1000.0 - } -} - /* Instants can wrap around and we want them to be comparable even after * wrapping around. This works by setting a maximum allowable difference * between two Instants to half the range. In checked_add and checked_sub, we @@ -163,6 +151,7 @@ impl Ord for Instant { /// A stopwatch is a utility designed for measuring the amount of time /// that elapses between its start and stop points. It can be used in various /// situations - animation timing, event timing, testing and debugging. +#[derive(Clone)] pub enum Stopwatch { Stopped(Duration), Running(Instant), @@ -214,7 +203,7 @@ impl Stopwatch { } } - /// Returns `true` if the stopwatch is currenly running. + /// Returns `true` if the stopwatch is currently running. pub fn is_running(&self) -> bool { matches!(*self, Self::Running(_)) } diff --git a/core/embed/rust/src/ui/component/label.rs b/core/embed/rust/src/ui/component/label.rs index 869f254dab..7061e659ff 100644 --- a/core/embed/rust/src/ui/component/label.rs +++ b/core/embed/rust/src/ui/component/label.rs @@ -104,6 +104,11 @@ impl<'a> Label<'a> { }; Rect::from_bottom_left_and_size(baseline, Offset::new(width, height)) } + + pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) { + self.text + .map(|c| self.layout.render_text_with_alpha(c, target, alpha)); + } } impl Component for Label<'_> { diff --git a/core/embed/rust/src/ui/component/text/layout.rs b/core/embed/rust/src/ui/component/text/layout.rs index 3655696b83..52a422ba58 100644 --- a/core/embed/rust/src/ui/component/text/layout.rs +++ b/core/embed/rust/src/ui/component/text/layout.rs @@ -239,10 +239,19 @@ impl TextLayout { /// Draw as much text as possible on the current screen. pub fn render_text2<'s>(&self, text: &str, target: &mut impl Renderer<'s>) -> LayoutFit { + self.render_text_with_alpha(text, target, 255) + } + /// Draw as much text as possible on the current screen. + pub fn render_text_with_alpha<'s>( + &self, + text: &str, + target: &mut impl Renderer<'s>, + alpha: u8, + ) -> LayoutFit { self.layout_text( text, &mut self.initial_cursor(), - &mut TextRenderer2::new(target), + &mut TextRenderer2::new(target).with_alpha(alpha), ) } @@ -541,16 +550,29 @@ impl LayoutSink for TextRenderer { } } -pub struct TextRenderer2<'a, 's, R>(pub &'a mut R, core::marker::PhantomData<&'s ()>) +pub struct TextRenderer2<'a, 's, R> where - R: Renderer<'s>; + R: Renderer<'s>, +{ + pub renderer: &'a mut R, + pd: core::marker::PhantomData<&'s ()>, + alpha: u8, +} impl<'a, 's, R> TextRenderer2<'a, 's, R> where R: Renderer<'s>, { pub fn new(target: &'a mut R) -> Self { - Self(target, core::marker::PhantomData) + Self { + renderer: target, + pd: core::marker::PhantomData, + alpha: 255, + } + } + + pub fn with_alpha(self, alpha: u8) -> Self { + Self { alpha, ..self } } } @@ -562,14 +584,16 @@ where shape::Text::new(cursor, text) .with_font(layout.style.text_font) .with_fg(layout.style.text_color) - .render(self.0); + .with_alpha(self.alpha) + .render(self.renderer); } fn hyphen(&mut self, cursor: Point, layout: &TextLayout) { shape::Text::new(cursor, "-") .with_font(layout.style.text_font) .with_fg(layout.style.hyphen_color) - .render(self.0); + .with_alpha(self.alpha) + .render(self.renderer); } fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) { @@ -578,12 +602,14 @@ where shape::ToifImage::new(bottom_left, icon.toif) .with_align(Alignment2D::BOTTOM_LEFT) .with_fg(layout.style.ellipsis_color) - .render(self.0); + .with_alpha(self.alpha) + .render(self.renderer); } else { shape::Text::new(cursor, ELLIPSIS) .with_font(layout.style.text_font) .with_fg(layout.style.ellipsis_color) - .render(self.0); + .with_alpha(self.alpha) + .render(self.renderer); } } @@ -592,12 +618,14 @@ where shape::ToifImage::new(cursor, icon.toif) .with_align(Alignment2D::BOTTOM_LEFT) .with_fg(layout.style.ellipsis_color) - .render(self.0); + .with_alpha(self.alpha) + .render(self.renderer); } else { shape::Text::new(cursor, ELLIPSIS) .with_font(layout.style.text_font) .with_fg(layout.style.ellipsis_color) - .render(self.0); + .with_alpha(self.alpha) + .render(self.renderer); } } } diff --git a/core/embed/rust/src/ui/model_mercury/component/button.rs b/core/embed/rust/src/ui/model_mercury/component/button.rs index 776d04f216..8ff864d91f 100644 --- a/core/embed/rust/src/ui/model_mercury/component/button.rs +++ b/core/embed/rust/src/ui/model_mercury/component/button.rs @@ -35,6 +35,7 @@ pub struct Button { state: State, long_press: Option, long_timer: Option, + haptic: bool, } impl Button { @@ -54,6 +55,7 @@ impl Button { state: State::Initial, long_press: None, long_timer: None, + haptic: true, } } @@ -102,6 +104,11 @@ impl Button { self } + pub fn without_haptics(mut self) -> Self { + self.haptic = false; + self + } + pub fn enable_if(&mut self, ctx: &mut EventCtx, enabled: bool) { if enabled { self.enable(ctx); @@ -182,7 +189,12 @@ impl Button { } } - pub fn render_background<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) { + pub fn render_background<'s>( + &self, + target: &mut impl Renderer<'s>, + style: &ButtonStyle, + alpha: u8, + ) { match &self.content { ButtonContent::IconBlend(_, _, _) => {} _ => { @@ -192,11 +204,13 @@ impl Button { .with_radius(self.radius.unwrap() as i16) .with_thickness(2) .with_fg(style.button_color) + .with_alpha(alpha) .render(target); } else { shape::Bar::new(self.area) .with_bg(style.button_color) .with_fg(style.button_color) + .with_alpha(alpha) .render(target); } } @@ -238,7 +252,12 @@ impl Button { } } - pub fn render_content<'s>(&self, target: &mut impl Renderer<'s>, style: &ButtonStyle) { + pub fn render_content<'s>( + &self, + target: &mut impl Renderer<'s>, + style: &ButtonStyle, + alpha: u8, + ) { match &self.content { ButtonContent::Empty => {} ButtonContent::Text(text) => { @@ -255,6 +274,7 @@ impl Button { .with_font(style.font) .with_fg(style.text_color) .with_align(self.text_align) + .with_alpha(alpha) .render(target); }); } @@ -262,24 +282,40 @@ impl Button { shape::ToifImage::new(self.area.center(), icon.toif) .with_align(Alignment2D::CENTER) .with_fg(style.icon_color) + .with_alpha(alpha) .render(target); } ButtonContent::IconAndText(child) => { - child.render(target, self.area, self.style(), Self::BASELINE_OFFSET); + child.render( + target, + self.area, + self.style(), + Self::BASELINE_OFFSET, + alpha, + ); } ButtonContent::IconBlend(bg, fg, offset) => { shape::Bar::new(self.area) .with_bg(style.background_color) + .with_alpha(alpha) .render(target); shape::ToifImage::new(self.area.top_left(), bg.toif) .with_fg(style.button_color) + .with_alpha(alpha) .render(target); shape::ToifImage::new(self.area.top_left() + *offset, fg.toif) .with_fg(style.icon_color) + .with_alpha(alpha) .render(target); } } } + + pub fn render_with_alpha<'s>(&self, target: &mut impl Renderer<'s>, alpha: u8) { + let style = self.style(); + self.render_background(target, style, alpha); + self.render_content(target, style, alpha); + } } impl Component for Button { @@ -307,7 +343,9 @@ impl Component for Button { // Touch started in our area, transform to `Pressed` state. if touch_area.contains(pos) { #[cfg(feature = "haptic")] - play(HapticEffect::ButtonPress); + if self.haptic { + play(HapticEffect::ButtonPress); + } self.set(ctx, State::Pressed); if let Some(duration) = self.long_press { self.long_timer = Some(ctx.request_timer(duration)); @@ -351,7 +389,9 @@ impl Component for Button { self.long_timer = None; if matches!(self.state, State::Pressed) { #[cfg(feature = "haptic")] - play(HapticEffect::ButtonPress); + if self.haptic { + play(HapticEffect::ButtonPress); + } self.set(ctx, State::Initial); return Some(ButtonMsg::LongPressed); } @@ -370,8 +410,8 @@ impl Component for Button { fn render<'s>(&self, target: &mut impl Renderer<'s>) { let style = self.style(); - self.render_background(target, style); - self.render_content(target, style); + self.render_background(target, style, 0xFF); + self.render_content(target, style, 0xFF); } #[cfg(feature = "ui_bounds")] @@ -605,6 +645,7 @@ impl IconText { area: Rect, style: &ButtonStyle, baseline_offset: Offset, + alpha: u8, ) { let width = self.text.map(|t| style.font.text_width(t)); @@ -635,6 +676,7 @@ impl IconText { shape::Text::new(text_pos, t) .with_font(style.font) .with_fg(style.text_color) + .with_alpha(alpha) .render(target) }); } @@ -643,6 +685,7 @@ impl IconText { shape::ToifImage::new(icon_pos, self.icon.toif) .with_align(Alignment2D::CENTER) .with_fg(style.icon_color) + .with_alpha(alpha) .render(target); } } diff --git a/core/embed/rust/src/ui/model_mercury/component/frame.rs b/core/embed/rust/src/ui/model_mercury/component/frame.rs index f558822d38..5e49511739 100644 --- a/core/embed/rust/src/ui/model_mercury/component/frame.rs +++ b/core/embed/rust/src/ui/model_mercury/component/frame.rs @@ -6,13 +6,13 @@ use crate::{ }, display::Icon, geometry::{Alignment, Insets, Rect}, + model_mercury::theme::TITLE_HEIGHT, shape::Renderer, }, }; use super::{theme, Button, ButtonMsg, ButtonStyleSheet, CancelInfoConfirmMsg, Footer}; -const TITLE_HEIGHT: i16 = 42; const BUTTON_EXPAND_BORDER: i16 = 32; #[derive(Clone)] @@ -24,6 +24,7 @@ pub struct Frame { button_msg: CancelInfoConfirmMsg, content: Child, footer: Option>, + overlapping_content: bool, } pub enum FrameMsg { @@ -46,6 +47,7 @@ where button_msg: CancelInfoConfirmMsg::Cancelled, content: Child::new(content), footer: None, + overlapping_content: false, } } @@ -188,7 +190,11 @@ where footer.place(footer_area); content_area = remaining; } - self.content.place(content_area); + if self.overlapping_content { + self.content.place(bounds); + } else { + self.content.place(content_area); + } bounds } @@ -209,15 +215,15 @@ where self.title.paint(); self.subtitle.paint(); self.button.paint(); - self.content.paint(); self.footer.paint(); + self.content.paint(); } fn render<'s>(&self, target: &mut impl Renderer<'s>) { self.title.render(target); self.subtitle.render(target); self.button.render(target); - self.content.render(target); self.footer.render(target); + self.content.render(target); } #[cfg(feature = "ui_bounds")] @@ -225,8 +231,8 @@ where self.title.bounds(sink); self.subtitle.bounds(sink); self.button.bounds(sink); - self.content.bounds(sink); self.footer.bounds(sink); + self.content.bounds(sink); } } diff --git a/core/embed/rust/src/ui/model_mercury/component/hold_to_confirm.rs b/core/embed/rust/src/ui/model_mercury/component/hold_to_confirm.rs new file mode 100644 index 0000000000..fc271db6ce --- /dev/null +++ b/core/embed/rust/src/ui/model_mercury/component/hold_to_confirm.rs @@ -0,0 +1,331 @@ +use crate::{ + time::Duration, + translations::TR, + ui::{ + component::{Component, Event, EventCtx}, + display::Color, + geometry::{Alignment2D, Offset, Rect}, + lerp::Lerp, + shape, + shape::Renderer, + }, +}; + +use super::{theme, Button, ButtonContent, ButtonMsg}; + +#[cfg(feature = "haptic")] +use crate::trezorhal::haptic::{self, HapticEffect}; +use crate::{ + time::Stopwatch, + ui::{ + component::Label, + constant::screen, + geometry::{Alignment, Point}, + model_mercury::theme::TITLE_HEIGHT, + }, +}; +use pareen; + +#[derive(Default, Clone)] +struct HoldToConfirmAnim { + pub timer: Stopwatch, +} + +impl HoldToConfirmAnim { + const DURATION_MS: u32 = 2200; + + pub fn is_active(&self) -> bool { + self.timer + .is_running_within(Duration::from_millis(Self::DURATION_MS)) + } + + pub fn eval(&self) -> f32 { + 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_header_opacity(&self, t: f32) -> u8 { + let header_opacity = pareen::constant(0.0).seq_ease_out( + 0.1, + easer::functions::Cubic, + 0.3, + pareen::constant(1.0), + ); + + u8::lerp(0, 255, header_opacity.eval(t)) + } + + pub fn get_header_opacity2(&self, t: f32) -> u8 { + let header_opacity2 = pareen::constant(1.0).seq_ease_in( + 2.0, + easer::functions::Cubic, + 0.2, + pareen::constant(0.0), + ); + + u8::lerp(0, 255, header_opacity2.eval(t)) + } + + pub fn get_circle_opacity(&self, t: f32) -> u8 { + let circle_opacity = pareen::constant(1.0).seq_ease_out( + 0.1, + easer::functions::Cubic, + 0.1, + pareen::constant(0.0), + ); + + u8::lerp(0, 255, circle_opacity.eval(t)) + } + + pub fn get_pad_color(&self, t: f32) -> Color { + let pad_color = pareen::constant(0.0).seq_ease_in_out( + 0.1, + easer::functions::Cubic, + 1.9, + pareen::constant(1.0), + ); + + Color::lerp(theme::GREY_EXTRA_DARK, theme::GREEN, pad_color.eval(t)) + } + + pub fn get_circle_max_height(&self, t: f32) -> i16 { + let circle_max_height = pareen::constant(0.0).seq_ease_in( + 0.1, + easer::functions::Cubic, + 1.5, + pareen::constant(1.0), + ); + + i16::lerp(266, 0, circle_max_height.eval(t)) + } + + pub fn get_circle_radius(&self, t: f32) -> i16 { + let circle_radius = pareen::constant(0.0).seq_ease_in( + 1.6, + easer::functions::Cubic, + 0.6, + pareen::constant(1.0), + ); + + i16::lerp(0, 100, circle_radius.eval(t)) + } + + pub fn get_haptic(&self, t: f32) -> u8 { + let haptic = pareen::constant(0.0).seq_ease_in( + 0.0, + easer::functions::Linear, + Self::DURATION_MS as f32 / 1000.0, + pareen::constant(1.0), + ); + + u8::lerp(0, 20, haptic.eval(t)) + } + + pub fn start(&mut self) { + self.timer.start(); + } + + pub fn reset(&mut self) { + self.timer = Stopwatch::new_stopped(); + } +} + +/// Component requesting a hold to confirm action from a user. Most typically +/// embedded as a content of a Frame. +#[derive(Clone)] +pub struct HoldToConfirm { + title: Label<'static>, + area: Rect, + button: Button, + circle_color: Color, + circle_pad_color: Color, + circle_inner_color: Color, + anim: HoldToConfirmAnim, +} + +#[derive(Clone)] +enum DismissType { + Tap, + Hold, +} + +impl HoldToConfirm { + pub fn new() -> Self { + let button = Button::new(ButtonContent::Empty) + .styled(theme::button_default()) + .with_long_press(Duration::from_millis(2200)) + .without_haptics(); + Self { + title: Label::new( + TR::instructions__continue_holding.into(), + Alignment::Start, + theme::label_title_main(), + ) + .vertically_centered(), + area: Rect::zero(), + circle_color: theme::GREEN, + circle_pad_color: theme::GREY_EXTRA_DARK, + circle_inner_color: theme::GREEN_LIGHT, + button, + anim: HoldToConfirmAnim::default(), + } + } +} + +impl Component for HoldToConfirm { + 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, + )); + self.title.place(screen().split_top(TITLE_HEIGHT).0); + 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) => { + self.anim.reset(); + ctx.request_anim_frame(); + ctx.request_paint(); + } + Some(ButtonMsg::LongPressed) => { + #[cfg(feature = "haptic")] + haptic::play(HapticEffect::HoldToConfirm); + return Some(()); + } + _ => (), + } + if self.anim.is_active() { + ctx.request_anim_frame(); + ctx.request_paint(); + } + None + } + + fn paint(&mut self) { + unimplemented!() + } + + fn render<'s>(&self, target: &mut impl Renderer<'s>) { + let elapsed = self.anim.eval(); + + shape::Bar::new(screen()) + .with_fg(theme::BLACK) + .with_bg(theme::BLACK) + .with_alpha(self.anim.get_parent_cover_opacity(elapsed)) + .render(target); + + let center = self.area.center(); + + const PROGRESS_CIRCLE_RADIUS: i16 = 88; + const PAD_RADIUS: i16 = 70; + const CIRCLE_RADIUS: i16 = 50; + const INNER_CIRCLE_RADIUS: i16 = 40; + const CIRCLE_THICKNESS: i16 = 2; + + if self.anim.get_parent_cover_opacity(elapsed) == 255 { + shape::Circle::new(center, PROGRESS_CIRCLE_RADIUS) + .with_fg(self.circle_inner_color) + .with_bg(theme::BLACK) + .with_thickness(CIRCLE_THICKNESS) + .render(target); + + shape::Bar::new(Rect::new( + Point::zero(), + Point::new(screen().width(), self.anim.get_circle_max_height(elapsed)), + )) + .with_fg(theme::BLACK) + .with_bg(theme::BLACK) + .render(target); + } + + let title_alpha = if self.anim.get_header_opacity2(elapsed) < 255 { + self.anim.get_header_opacity2(elapsed) + } else { + self.anim.get_header_opacity(elapsed) + }; + + self.title.render_with_alpha(target, title_alpha); + + let pad_color = self.anim.get_pad_color(elapsed); + let circle_alpha = self.anim.get_circle_opacity(elapsed); + + shape::Circle::new(center, PAD_RADIUS) + .with_fg(pad_color) + .with_bg(pad_color) + .render(target); + shape::Circle::new(center, CIRCLE_RADIUS) + .with_fg(self.circle_color) + .with_bg(self.circle_pad_color) + .with_thickness(CIRCLE_THICKNESS) + .with_alpha(circle_alpha) + .render(target); + shape::Circle::new(center, CIRCLE_RADIUS - CIRCLE_THICKNESS) + .with_fg(self.circle_pad_color) + .with_bg(self.circle_pad_color) + .with_thickness(CIRCLE_RADIUS - CIRCLE_THICKNESS - INNER_CIRCLE_RADIUS) + .with_alpha(circle_alpha) + .render(target); + shape::Circle::new(center, INNER_CIRCLE_RADIUS) + .with_fg(self.circle_inner_color) + .with_bg(theme::BLACK) + .with_thickness(CIRCLE_THICKNESS) + .with_alpha(circle_alpha) + .render(target); + + shape::ToifImage::new(center, theme::ICON_SIGN.toif) + .with_fg(theme::GREY) + .with_alpha(circle_alpha) + .with_align(Alignment2D::CENTER) + .render(target); + + shape::Circle::new(center, self.anim.get_circle_radius(elapsed)) + .with_fg(theme::BLACK) + .render(target); + + #[cfg(feature = "haptic")] + { + let hap = self.anim.get_haptic(elapsed); + if hap > 0 { + haptic::play_custom(hap as i8, 100); + } + } + } +} + +#[cfg(feature = "micropython")] +impl crate::ui::flow::Swipable<()> for HoldToConfirm {} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for HoldToConfirm { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("StatusScreen"); + t.child("button", &self.button); + } +} 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 2d71d7ac6a..3e2285b2e3 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -12,6 +12,9 @@ mod vertical_menu; mod fido_icons; mod error; mod frame; + +#[cfg(feature = "translations")] +mod hold_to_confirm; #[cfg(feature = "micropython")] mod homescreen; #[cfg(feature = "translations")] @@ -48,6 +51,8 @@ pub use error::ErrorScreen; pub use fido::{FidoConfirm, FidoMsg}; pub use footer::Footer; pub use frame::{Frame, FrameMsg}; +#[cfg(feature = "translations")] +pub use hold_to_confirm::HoldToConfirm; #[cfg(feature = "micropython")] pub use homescreen::{check_homescreen_format, Homescreen, HomescreenMsg, Lockscreen}; #[cfg(feature = "translations")] 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 061a86e041..fcbd39ca37 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 @@ -133,7 +133,8 @@ impl Component for PromptScreen { .with_thickness(2) .render(target); }); - self.button.render_content(target, self.button.style()); + self.button + .render_content(target, self.button.style(), 0xff); } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_create.rs b/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_create.rs index 1afbc92621..27d1f137f9 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_create.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/confirm_reset_create.rs @@ -1,5 +1,6 @@ use crate::{ error, + micropython::{map::Map, obj::Obj, util}, strutil::TString, translations::TR, ui::{ @@ -9,11 +10,12 @@ use crate::{ ButtonRequestExt, ComponentExt, SwipeDirection, }, flow::{base::Decision, flow_store, FlowMsg, FlowState, FlowStore, SwipeFlow, SwipePage}, + layout::obj::LayoutObj, }, }; use super::super::{ - component::{Frame, FrameMsg, PromptScreen, VerticalMenu, VerticalMenuChoiceMsg}, + component::{Frame, FrameMsg, HoldToConfirm, VerticalMenu, VerticalMenuChoiceMsg}, theme, }; @@ -60,11 +62,6 @@ impl FlowState for ConfirmResetCreate { } } -use crate::{ - micropython::{map::Map, obj::Obj, util}, - ui::layout::obj::LayoutObj, -}; - #[allow(clippy::not_unsafe_ptr_arg_deref)] pub extern "C" fn new_confirm_reset_create( n_args: usize, @@ -100,16 +97,16 @@ impl ConfirmResetCreate { FrameMsg::Button(_) => Some(FlowMsg::Cancelled), }); - let content_confirm = Frame::left_aligned( - TR::reset__title_create_wallet.into(), - PromptScreen::new_hold_to_confirm(), - ) - .with_footer(TR::instructions__hold_to_confirm.into(), None) - .map(|msg| match msg { - FrameMsg::Content(()) => Some(FlowMsg::Confirmed), - _ => Some(FlowMsg::Cancelled), - }) - .one_button_request(ButtonRequestCode::ResetDevice.with_type("confirm_setup_device")); + let content_confirm = + Frame::left_aligned(TR::reset__title_create_wallet.into(), HoldToConfirm::new()) + .with_footer(TR::instructions__hold_to_confirm.into(), None) + .map(|msg| match msg { + FrameMsg::Content(()) => Some(FlowMsg::Confirmed), + _ => Some(FlowMsg::Cancelled), + }) + .one_button_request( + ButtonRequestCode::ResetDevice.with_type("confirm_setup_device"), + ); let store = flow_store() .add(content_intro)? diff --git a/core/embed/rust/src/ui/model_mercury/theme/mod.rs b/core/embed/rust/src/ui/model_mercury/theme/mod.rs index 616ab01b64..71d8d639b7 100644 --- a/core/embed/rust/src/ui/model_mercury/theme/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/theme/mod.rs @@ -794,6 +794,7 @@ pub const TEXT_CHECKLIST_DONE: TextStyle = TextStyle::new(Font::SUB, GREY, BG, G /// the header. [px] pub const SPACING: i16 = 2; +pub const TITLE_HEIGHT: i16 = 42; pub const CONTENT_BORDER: i16 = 0; pub const BUTTON_HEIGHT: i16 = 62; pub const BUTTON_WIDTH: i16 = 78; diff --git a/core/translations/en.json b/core/translations/en.json index ef473a2590..ddb44814b0 100644 --- a/core/translations/en.json +++ b/core/translations/en.json @@ -388,6 +388,8 @@ "instructions__swipe_up": "Swipe up", "instructions__tap_to_confirm": "Tap to confirm", "instructions__tap_to_start": "Tap to start", + "instructions__hold_to_confirm": "Hold to confirm", + "instructions__continue_holding": "Continue\nholding", "joint__title": "Joint transaction", "joint__to_the_total_amount": "To the total amount:", "joint__you_are_contributing": "You are contributing:",