From 30ca8bdd62228afdd50ebce6790ee158002c6e94 Mon Sep 17 00:00:00 2001 From: Martin Milata Date: Thu, 16 May 2024 13:22:40 +0200 Subject: [PATCH] feat(core/ui): T3T1 share words animation [no changelog] --- core/embed/rust/src/ui/component/swipe.rs | 13 +- core/embed/rust/src/ui/flow/base.rs | 16 +-- core/embed/rust/src/ui/flow/page.rs | 30 ++--- core/embed/rust/src/ui/flow/swipe.rs | 36 ++--- core/embed/rust/src/ui/geometry.rs | 13 +- core/embed/rust/src/ui/lerp.rs | 2 + .../model_mercury/component/homescreen/mod.rs | 4 +- .../src/ui/model_mercury/component/mod.rs | 2 +- .../ui/model_mercury/component/share_words.rs | 123 +++++++++++------- .../ui/model_mercury/flow/show_share_words.rs | 14 +- core/embed/rust/src/ui/util.rs | 27 ++++ 11 files changed, 163 insertions(+), 117 deletions(-) diff --git a/core/embed/rust/src/ui/component/swipe.rs b/core/embed/rust/src/ui/component/swipe.rs index 876d6209ec..5cdaf7c18d 100644 --- a/core/embed/rust/src/ui/component/swipe.rs +++ b/core/embed/rust/src/ui/component/swipe.rs @@ -1,7 +1,7 @@ use crate::ui::{ component::{Component, Event, EventCtx}, event::TouchEvent, - geometry::{Point, Rect}, + geometry::{Offset, Point, Rect}, shape::Renderer, }; @@ -13,6 +13,17 @@ pub enum SwipeDirection { Right, } +impl SwipeDirection { + pub fn as_offset(self, size: Offset) -> Offset { + match self { + SwipeDirection::Up => Offset::y(-size.y), + SwipeDirection::Down => Offset::y(size.y), + SwipeDirection::Left => Offset::x(-size.x), + SwipeDirection::Right => Offset::x(size.x), + } + } +} + /// Copy of `model_tt/component/swipe.rs` but without the backlight handling. pub struct Swipe { pub area: Rect, diff --git a/core/embed/rust/src/ui/flow/base.rs b/core/embed/rust/src/ui/flow/base.rs index 171e985aa2..d24f5982f2 100644 --- a/core/embed/rust/src/ui/flow/base.rs +++ b/core/embed/rust/src/ui/flow/base.rs @@ -1,20 +1,6 @@ -use crate::ui::{ - component::{EventCtx, SwipeDirection}, - geometry::Offset, -}; +use crate::ui::component::{EventCtx, SwipeDirection}; use num_traits::ToPrimitive; -impl SwipeDirection { - pub fn as_offset(self, size: Offset) -> Offset { - match self { - SwipeDirection::Up => Offset::y(-size.y), - SwipeDirection::Down => Offset::y(size.y), - SwipeDirection::Left => Offset::x(-size.x), - SwipeDirection::Right => Offset::x(size.x), - } - } -} - /// Component must implement this trait in order to be part of swipe-based flow. /// /// Default implementation ignores every swipe. diff --git a/core/embed/rust/src/ui/flow/page.rs b/core/embed/rust/src/ui/flow/page.rs index e34ce9196a..0e5e5cbcd7 100644 --- a/core/embed/rust/src/ui/flow/page.rs +++ b/core/embed/rust/src/ui/flow/page.rs @@ -1,11 +1,11 @@ use crate::{ micropython::gc::Gc, - time::{Duration, Instant}, + time::Instant, ui::{ animation::Animation, component::{Component, Event, EventCtx, Paginate, SwipeDirection}, flow::base::Swipable, - geometry::{Axis, Offset, Rect}, + geometry::{Axis, Rect}, shape::Renderer, util, }, @@ -15,13 +15,11 @@ pub struct Transition { /// Clone of the component before page change. cloned: Gc, /// Animation progress. - animation: Animation, + animation: Animation, /// Direction of the slide animation. direction: SwipeDirection, } -const ANIMATION_DURATION: Duration = Duration::from_millis(333); - /// Allows any implementor of `Paginate` to be part of `Swipable` UI flow. /// Renders sliding animation when changing pages. pub struct SwipePage { @@ -74,16 +72,13 @@ impl SwipePage { transition: &'s Transition, target: &mut impl Renderer<'s>, ) { - let off = transition.animation.value(Instant::now()); target.in_clip(self.bounds, &|target| { - target.with_origin(off, &|target| { - transition.cloned.render(target); - }); - target.with_origin( - off - transition.direction.as_offset(self.bounds.size()), - &|target| { - self.inner.render(target); - }, + util::render_slide( + |target| transition.cloned.render(target), + |target| self.inner.render(target), + transition.animation.value(Instant::now()), + transition.direction, + target, ); }); } @@ -147,12 +142,7 @@ impl Swipable for SwipePage { } self.transition = Some(Transition { cloned: unwrap!(Gc::new(self.inner.clone())), - animation: Animation::new( - Offset::zero(), - direction.as_offset(self.bounds.size()), - ANIMATION_DURATION, - Instant::now(), - ), + animation: Animation::new(0.0f32, 1.0f32, util::SLIDE_DURATION, Instant::now()), direction, }); self.inner.change_page(self.current); diff --git a/core/embed/rust/src/ui/flow/swipe.rs b/core/embed/rust/src/ui/flow/swipe.rs index 599026f899..4b154da7a6 100644 --- a/core/embed/rust/src/ui/flow/swipe.rs +++ b/core/embed/rust/src/ui/flow/swipe.rs @@ -1,6 +1,6 @@ use crate::{ error, - time::{Duration, Instant}, + time::Instant, ui::{ animation::Animation, component::{Component, Event, EventCtx, Swipe, SwipeDirection}, @@ -11,8 +11,6 @@ use crate::{ }, }; -const ANIMATION_DURATION: Duration = Duration::from_millis(333); - /// Given a state enum and a corresponding FlowStore, create a Component that /// implements a swipe navigation between the states with animated transitions. /// @@ -28,8 +26,6 @@ pub struct SwipeFlow { transition: Transition, /// Swipe detector. swipe: Swipe, - /// Animation parameter. - anim_offset: Offset, } enum Transition { @@ -38,7 +34,7 @@ enum Transition { /// State we are transitioning _from_. prev_state: Q, /// Animation progress. - animation: Animation, + animation: Animation, /// Direction of the slide animation. direction: SwipeDirection, }, @@ -55,7 +51,6 @@ impl SwipeFlow { store, transition: Transition::None, swipe: Swipe::new().down().up().left().right(), - anim_offset: Offset::zero(), }) } @@ -72,12 +67,7 @@ impl SwipeFlow { } self.transition = Transition::External { prev_state: self.state, - animation: Animation::new( - Offset::zero(), - direction.as_offset(self.anim_offset), - ANIMATION_DURATION, - Instant::now(), - ), + animation: Animation::new(0.0f32, 1.0f32, util::SLIDE_DURATION, Instant::now()), direction, }; self.state = state; @@ -92,17 +82,17 @@ impl SwipeFlow { fn render_transition<'s>( &'s self, prev_state: &Q, - animation: &Animation, + animation: &Animation, direction: &SwipeDirection, target: &mut impl Renderer<'s>, ) { - let off = animation.value(Instant::now()); - target.with_origin(off, &|target| { - self.render_state(*prev_state, target); - }); - target.with_origin(off - direction.as_offset(self.anim_offset), &|target| { - self.render_state(self.state, target); - }); + util::render_slide( + |target| self.render_state(*prev_state, target), + |target| self.render_state(self.state, target), + animation.value(Instant::now()), + *direction, + target, + ); } fn handle_transition(&mut self, ctx: &mut EventCtx, event: Event) -> Option { @@ -164,10 +154,6 @@ impl Component for SwipeFlow { type Msg = FlowMsg; fn place(&mut self, bounds: Rect) -> Rect { - // Save screen size for slide animation. Once we have reasonable constants trait - // this can be set in the constructor. - self.anim_offset = bounds.size(); - self.swipe.place(bounds); self.store.place(bounds) } diff --git a/core/embed/rust/src/ui/geometry.rs b/core/embed/rust/src/ui/geometry.rs index bca2e523c0..01f26b2688 100644 --- a/core/embed/rust/src/ui/geometry.rs +++ b/core/embed/rust/src/ui/geometry.rs @@ -1,5 +1,5 @@ use crate::ui::lerp::Lerp; -use core::ops::{Add, Neg, Sub}; +use core::ops::{Add, Mul, Neg, Sub}; const fn min(a: i16, b: i16) -> i16 { if a < b { @@ -128,6 +128,17 @@ impl Sub for Offset { } } +impl Mul for Offset { + type Output = Offset; + + fn mul(self, rhs: f32) -> Self::Output { + Offset::new( + (f32::from(self.x) * rhs) as i16, + (f32::from(self.y) * rhs) as i16, + ) + } +} + impl From for Offset { fn from(val: Point) -> Self { Offset::new(val.x, val.y) diff --git a/core/embed/rust/src/ui/lerp.rs b/core/embed/rust/src/ui/lerp.rs index 34d3802197..3daa8d5277 100644 --- a/core/embed/rust/src/ui/lerp.rs +++ b/core/embed/rust/src/ui/lerp.rs @@ -71,6 +71,8 @@ impl_lerp_for_uint!(u8); impl_lerp_for_uint!(u16); impl_lerp_for_uint!(u32); +impl_lerp_for_int!(f32); + #[cfg(test)] mod tests { use super::*; diff --git a/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs b/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs index 69737881ea..d9ec8b0e22 100644 --- a/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/homescreen/mod.rs @@ -7,10 +7,8 @@ use crate::{ ui::{ component::{Component, Event, EventCtx, TimerToken}, display::{ - self, image::{ImageInfo, ToifFormat}, - tjpgd::jpeg_info, - toif::{Icon, Toif}, + toif::Icon, Color, Font, }, event::{TouchEvent, USBEvent}, 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 78e36aab02..4a12e9a442 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -67,7 +67,7 @@ pub use prompt_screen::PromptScreen; pub use result::{ResultFooter, ResultScreen, ResultStyle}; pub use scroll::ScrollBar; #[cfg(feature = "translations")] -pub use share_words::{ShareWords, ShareWordsMsg}; +pub use share_words::ShareWords; pub use simple_page::SimplePage; pub use status_screen::StatusScreen; pub use swipe::{Swipe, SwipeDirection}; diff --git a/core/embed/rust/src/ui/model_mercury/component/share_words.rs b/core/embed/rust/src/ui/model_mercury/component/share_words.rs index 6736f929af..8d681f5d85 100644 --- a/core/embed/rust/src/ui/model_mercury/component/share_words.rs +++ b/core/embed/rust/src/ui/model_mercury/component/share_words.rs @@ -1,19 +1,23 @@ use super::theme; use crate::{ strutil::TString, + time::{Duration, Instant}, translations::TR, ui::{ - component::{Component, Event, EventCtx, Paginate, Swipe, SwipeDirection}, + animation::Animation, + component::{Component, Event, EventCtx, Never, SwipeDirection}, flow::Swipable, geometry::{Alignment, Alignment2D, Insets, Offset, Rect}, model_mercury::component::Footer, shape, shape::Renderer, + util, }, }; use heapless::{String, Vec}; const MAX_WORDS: usize = 33; // super-shamir has 33 words, all other have less +const ANIMATION_DURATION: Duration = Duration::from_millis(166); /// Component showing mnemonic/share words during backup procedure. Model T3T1 /// contains one word per screen. A user is instructed to swipe up/down to see @@ -22,19 +26,15 @@ pub struct ShareWords<'a> { area: Rect, share_words: Vec, MAX_WORDS>, page_index: usize, + prev_index: usize, /// Area reserved for a shown word from mnemonic/share area_word: Rect, - /// TODO: review when swipe concept done for T3T1 - swipe: Swipe, + /// `Some` when transition animation is in progress + animation: Option>, /// Footer component for instructions and word counting footer: Footer<'static>, } -pub enum ShareWordsMsg { - GoPrevScreen, - WordsSeen, -} - impl<'a> ShareWords<'a> { const AREA_WORD_HEIGHT: i16 = 91; @@ -43,8 +43,9 @@ impl<'a> ShareWords<'a> { area: Rect::zero(), share_words, page_index: 0, + prev_index: 0, area_word: Rect::zero(), - swipe: Swipe::new().up().down(), + animation: None, footer: Footer::new(TR::instructions__swipe_up), } } @@ -56,10 +57,23 @@ impl<'a> ShareWords<'a> { fn is_final_page(&self) -> bool { self.page_index == self.share_words.len() - 1 } + + fn render_word<'s>(&'s self, word_index: usize, target: &mut impl Renderer<'s>) { + // the share word + let word = self.share_words[word_index]; + let word_baseline = target.viewport().clip.center() + + Offset::y(theme::TEXT_SUPER.text_font.visible_text_height("A") / 2); + word.map(|w| { + shape::Text::new(word_baseline, w) + .with_font(theme::TEXT_SUPER.text_font) + .with_align(Alignment::Center) + .render(target); + }); + } } impl<'a> Component for ShareWords<'a> { - type Msg = ShareWordsMsg; + type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { self.area = bounds; @@ -76,29 +90,18 @@ impl<'a> Component for ShareWords<'a> { self.footer .place(used_area.split_bottom(Footer::HEIGHT_SIMPLE).1); - self.swipe.place(bounds); // Swipe possible on the whole screen area self.area } - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + fn event(&mut self, ctx: &mut EventCtx, _event: Event) -> Option { // ctx.set_page_count(self.share_words.len()); - let swipe = self.swipe.event(ctx, event); - match swipe { - Some(SwipeDirection::Up) => { - if self.is_final_page() { - return Some(ShareWordsMsg::WordsSeen); - } - self.change_page(self.page_index + 1); - ctx.request_paint(); + if let Some(a) = &self.animation { + if a.finished(Instant::now()) { + self.animation = None; + } else { + ctx.request_anim_frame(); } - Some(SwipeDirection::Down) => { - if self.is_first_page() { - return Some(ShareWordsMsg::GoPrevScreen); - } - self.change_page(self.page_index.saturating_sub(1)); - ctx.request_paint(); - } - _ => (), + ctx.request_paint(); } None } @@ -129,16 +132,25 @@ impl<'a> Component for ShareWords<'a> { .with_fg(theme::GREY) .render(target); - // the share word - let word = self.share_words[self.page_index]; - let word_baseline = self.area_word.center() - + Offset::y(theme::TEXT_SUPER.text_font.visible_text_height("A") / 2); - word.map(|w| { - shape::Text::new(word_baseline, w) - .with_font(theme::TEXT_SUPER.text_font) - .with_align(Alignment::Center) - .render(target); - }); + if let Some(animation) = &self.animation { + target.in_clip(self.area_word, &|target| { + util::render_slide( + |target| self.render_word(self.prev_index, target), + |target| self.render_word(self.page_index, target), + animation.value(Instant::now()), + if self.prev_index < self.page_index { + SwipeDirection::Up + } else { + SwipeDirection::Down + }, + target, + ) + }); + } else { + target.in_clip(self.area_word, &|target| { + self.render_word(self.page_index, target); + }) + }; // footer with instructions self.footer.render(target); @@ -148,15 +160,36 @@ impl<'a> Component for ShareWords<'a> { fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {} } -impl<'a> Swipable for ShareWords<'a> {} - -impl<'a> Paginate for ShareWords<'a> { - fn page_count(&mut self) -> usize { - self.share_words.len() +impl<'a> Swipable for ShareWords<'a> { + fn swipe_start(&mut self, ctx: &mut EventCtx, direction: SwipeDirection) -> bool { + match direction { + SwipeDirection::Up if !self.is_final_page() => { + self.prev_index = self.page_index; + self.page_index = (self.page_index + 1).min(self.share_words.len() - 1); + } + SwipeDirection::Down if !self.is_first_page() => { + self.prev_index = self.page_index; + self.page_index = self.page_index.saturating_sub(1); + } + _ => return false, + }; + if util::animation_disabled() { + ctx.request_paint(); + return true; + } + self.animation = Some(Animation::new( + 0.0f32, + 1.0f32, + ANIMATION_DURATION, + Instant::now(), + )); + ctx.request_anim_frame(); + ctx.request_paint(); + true } - fn change_page(&mut self, active_page: usize) { - self.page_index = active_page; + fn swipe_finished(&self) -> bool { + self.animation.is_none() } } diff --git a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs index 764b0efb89..642bba34fe 100644 --- a/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs +++ b/core/embed/rust/src/ui/model_mercury/flow/show_share_words.rs @@ -15,7 +15,7 @@ use crate::{ use heapless::Vec; use super::super::{ - component::{Frame, FrameMsg, PromptScreen, ShareWords, ShareWordsMsg, SwipeDirection}, + component::{Frame, FrameMsg, PromptScreen, ShareWords, SwipeDirection}, theme, }; @@ -37,6 +37,12 @@ impl FlowState for ShowShareWords { (ShowShareWords::Confirm, SwipeDirection::Down) => { Decision::Goto(ShowShareWords::Words, direction) } + (ShowShareWords::Words, SwipeDirection::Up) => { + Decision::Goto(ShowShareWords::Confirm, direction) + } + (ShowShareWords::Words, SwipeDirection::Down) => { + Decision::Goto(ShowShareWords::Instruction, direction) + } (ShowShareWords::CheckBackupIntro, SwipeDirection::Up) => { Decision::Return(FlowMsg::Confirmed) } @@ -85,11 +91,7 @@ impl ShowShareWords { .map(|msg| matches!(msg, FrameMsg::Content(_)).then_some(FlowMsg::Confirmed)); let content_words = - Frame::left_aligned(title, ShareWords::new(share_words_vec)).map(|msg| match msg { - FrameMsg::Content(ShareWordsMsg::GoPrevScreen) => Some(FlowMsg::Cancelled), - FrameMsg::Content(ShareWordsMsg::WordsSeen) => Some(FlowMsg::Confirmed), - _ => None, - }); + Frame::left_aligned(title, ShareWords::new(share_words_vec)).map(|_| None); let content_confirm = Frame::left_aligned(text_confirm, PromptScreen::new_hold_to_confirm()) diff --git a/core/embed/rust/src/ui/util.rs b/core/embed/rust/src/ui/util.rs index f3e4f33fba..f5b427b9b5 100644 --- a/core/embed/rust/src/ui/util.rs +++ b/core/embed/rust/src/ui/util.rs @@ -1,5 +1,6 @@ use crate::{ strutil::{ShortString, TString}, + time::Duration, ui::{ component::text::TextStyle, display, @@ -179,6 +180,32 @@ macro_rules! include_icon { }; } +pub const SLIDE_DURATION: Duration = Duration::from_millis(333); + +#[cfg(feature = "new_rendering")] +pub fn render_slide<'s, F0, F1, R>( + render_old: F0, + render_new: F1, + progress: f32, + direction: crate::ui::component::SwipeDirection, + target: &mut R, +) where + R: crate::ui::shape::Renderer<'s>, + F0: Fn(&mut R), + F1: Fn(&mut R), +{ + let bounds = target.viewport().clip; + let full_offset = direction.as_offset(bounds.size()); + let current_offset = full_offset * progress; + + target.with_origin(current_offset, &|target| { + render_old(target); + }); + target.with_origin(current_offset - full_offset, &|target| { + render_new(target); + }); +} + #[cfg(test)] mod tests { use crate::strutil;