diff --git a/core/embed/rust/src/ui/model_mercury/component/footer.rs b/core/embed/rust/src/ui/model_mercury/component/footer.rs index cd2f069345..1235763740 100644 --- a/core/embed/rust/src/ui/model_mercury/component/footer.rs +++ b/core/embed/rust/src/ui/model_mercury/component/footer.rs @@ -12,8 +12,6 @@ use crate::{ }, }; -use heapless::String; - /// Component showing a task instruction, e.g. "Swipe up", and an optional /// content consisting of one of these: /// - a task description e.g. "Confirm transaction", or @@ -94,6 +92,8 @@ impl<'a> Footer<'a> { if let Some(ref mut content) = self.content { if let Some(counter) = content.as_page_counter_mut() { counter.update_current_page(n); + self.swipe_allow_down = counter.is_first_page(); + self.swipe_allow_up = counter.is_last_page(); ctx.request_paint(); } else { #[cfg(feature = "ui_debug")] @@ -284,9 +284,9 @@ impl<'a> FooterContent<'a> { #[derive(Clone)] struct PageCounter { area: Rect, + font: Font, page_curr: u8, page_max: u8, - font: Font, } impl PageCounter { @@ -300,7 +300,15 @@ impl PageCounter { } fn update_current_page(&mut self, new_value: u8) { - self.page_curr = new_value; + self.page_curr = new_value.clamp(1, self.page_max); + } + + fn is_first_page(&self) -> bool { + self.page_curr == 1 + } + + fn is_last_page(&self) -> bool { + self.page_curr == self.page_max } } 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 760367fa02..c20c79b654 100644 --- a/core/embed/rust/src/ui/model_mercury/component/mod.rs +++ b/core/embed/rust/src/ui/model_mercury/component/mod.rs @@ -78,7 +78,7 @@ pub use set_brightness::SetBrightnessDialog; #[cfg(feature = "translations")] pub use share_words::ShareWords; pub use status_screen::StatusScreen; -pub use swipe_content::SwipeContent; +pub use swipe_content::{InternallySwipable, InternallySwipableContent, SwipeContent}; #[cfg(feature = "translations")] pub use swipe_up_screen::{SwipeUpScreen, SwipeUpScreenMsg}; #[cfg(feature = "translations")] 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 eb959847b1..7054104d02 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,4 +1,4 @@ -use super::theme; +use super::{theme, InternallySwipableContent}; use crate::{ strutil::TString, time::Duration, @@ -11,7 +11,7 @@ use crate::{ }, event::SwipeEvent, geometry::{Alignment, Alignment2D, Insets, Offset, Rect}, - model_mercury::component::{Frame, FrameMsg}, + model_mercury::component::{Frame, FrameMsg, InternallySwipable}, shape::{self, Renderer}, util, }, @@ -30,7 +30,7 @@ type IndexVec = Vec; /// words are rendered within `ShareWordsInner` component, pub struct ShareWords<'a> { subtitle: TString<'static>, - frame: Frame>, + frame: Frame>>, repeated_indices: Option, } @@ -49,12 +49,15 @@ impl<'a> ShareWords<'a> { let n_words = share_words.len(); Self { subtitle, - frame: Frame::left_aligned(title, ShareWordsInner::new(share_words)) - .with_swipe(SwipeDirection::Up, SwipeSettings::default()) - .with_swipe(SwipeDirection::Down, SwipeSettings::default()) - .with_vertical_pages() - .with_subtitle(subtitle) - .with_footer_counter(TR::instructions__swipe_up.into(), n_words as u8), + frame: Frame::left_aligned( + title, + InternallySwipableContent::new(ShareWordsInner::new(share_words)), + ) + .with_swipe(SwipeDirection::Up, SwipeSettings::default()) + .with_swipe(SwipeDirection::Down, SwipeSettings::default()) + .with_vertical_pages() + .with_subtitle(subtitle) + .with_footer_counter(TR::instructions__swipe_up.into(), n_words as u8), repeated_indices, } } @@ -81,7 +84,7 @@ impl<'a> Component for ShareWords<'a> { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - let page_index = self.frame.inner().page_index as u8; + let page_index = self.frame.inner().inner().page_index as u8; if let Some(repeated_indices) = &self.repeated_indices { if repeated_indices.contains(&page_index) { let updated_subtitle = TString::from_translation(TR::reset__the_word_is_repeated); @@ -160,14 +163,14 @@ impl<'a> ShareWordsInner<'a> { self.page_index == self.share_words.len() as i16 - 1 } - fn render_word<'s>(&self, word_index: i16, target: &mut impl Renderer<'s>) { + fn render_word<'s>(&self, word_index: i16, target: &mut impl Renderer<'s>, area: Rect) { // the share word if word_index >= self.share_words.len() as _ || word_index < 0 { return; } let word = self.share_words[word_index as usize]; - let word_baseline = target.viewport().clip.center() - + Offset::y(theme::TEXT_SUPER.text_font.visible_text_height("A") / 2); + let word_baseline = + area.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) @@ -260,7 +263,16 @@ impl<'a> Component for ShareWordsInner<'a> { .with_fg(theme::GREY) .render(target); - if self.progress > 0 { + let (dir, should_animate) = if self.page_index < self.next_index { + ( + SwipeDirection::Up, + self.page_index < self.share_words.len() as i16 - 1, + ) + } else { + (SwipeDirection::Down, self.page_index > 0) + }; + + if self.progress > 0 && should_animate { target.in_clip(self.area_word, &|target| { let progress = pareen::constant(0.0).seq_ease_out( 0.0, @@ -270,21 +282,15 @@ impl<'a> Component for ShareWordsInner<'a> { ); util::render_slide( - |target| self.render_word(self.page_index, target), - |target| self.render_word(self.next_index, target), + |target| self.render_word(self.page_index, target, target.viewport().clip), + |target| self.render_word(self.next_index, target, target.viewport().clip), progress.eval(self.progress as f32 / 1000.0), - if self.page_index < self.next_index { - SwipeDirection::Up - } else { - SwipeDirection::Down - }, + dir, target, ) }); } else { - target.in_clip(self.area_word, &|target| { - self.render_word(self.page_index, target); - }) + self.render_word(self.page_index, target, self.area_word); }; } @@ -292,6 +298,16 @@ impl<'a> Component for ShareWordsInner<'a> { fn bounds(&self, _sink: &mut dyn FnMut(Rect)) {} } +impl InternallySwipable for ShareWordsInner<'_> { + fn current_page(&self) -> usize { + self.page_index as usize + } + + fn num_pages(&self) -> usize { + self.share_words.len() + } +} + #[cfg(feature = "ui_debug")] impl<'a> crate::trace::Trace for ShareWordsInner<'a> { fn trace(&self, t: &mut dyn crate::trace::Tracer) { diff --git a/core/embed/rust/src/ui/model_mercury/component/swipe_content.rs b/core/embed/rust/src/ui/model_mercury/component/swipe_content.rs index db09c50750..d1bc9ac466 100644 --- a/core/embed/rust/src/ui/model_mercury/component/swipe_content.rs +++ b/core/embed/rust/src/ui/model_mercury/component/swipe_content.rs @@ -119,21 +119,14 @@ impl SwipeContent { pub fn inner(&self) -> &T { &self.inner } -} -impl Component for SwipeContent { - type Msg = T::Msg; - - fn place(&mut self, bounds: Rect) -> Rect { - self.bounds = self.inner.place(bounds); - self.bounds - } - - fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + fn process_event(&mut self, ctx: &mut EventCtx, event: Event, animate: bool) -> Option { if let Event::Attach(attach_type) = event { self.progress = 0; - if self.show_attach_anim { + if self.show_attach_anim && animate { self.attach_type = Some(attach_type); + } else { + self.attach_type = None; } self.attach_animation.reset(); ctx.request_anim_frame(); @@ -153,8 +146,10 @@ impl Component for SwipeContent { if let Event::Swipe(SwipeEvent::Move(dir, progress)) = event { match dir { SwipeDirection::Up | SwipeDirection::Down => { - self.dir = dir; - self.progress = progress; + if animate { + self.dir = dir; + self.progress = progress; + } } _ => {} } @@ -172,6 +167,19 @@ impl Component for SwipeContent { _ => self.inner.event(ctx, event), } } +} + +impl Component for SwipeContent { + type Msg = T::Msg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.bounds = self.inner.place(bounds); + self.bounds + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + self.process_event(ctx, event, true) + } fn paint(&mut self) { self.inner.paint() @@ -259,3 +267,118 @@ where t.child("content", &self.inner); } } + +pub trait InternallySwipable { + fn current_page(&self) -> usize; + + fn num_pages(&self) -> usize; +} + +pub struct InternallySwipableContent +where + T: Component + InternallySwipable, +{ + content: SwipeContent, + animate: bool, +} + +impl InternallySwipableContent +where + T: Component + InternallySwipable, +{ + pub fn new(content: T) -> Self { + Self { + content: SwipeContent::new(content), + animate: true, + } + } + + pub fn inner(&self) -> &T { + self.content.inner() + } + + fn should_animate_attach(&self, attach_type: AttachType) -> bool { + let is_first_page = self.content.inner.current_page() == 0; + let is_last_page = + self.content.inner.current_page() == (self.content.inner.num_pages() - 1); + + let is_swipe_up = matches!(attach_type, AttachType::Swipe(SwipeDirection::Up)); + let is_swipe_down = matches!(attach_type, AttachType::Swipe(SwipeDirection::Down)); + + if !self.content.show_attach_anim { + return false; + } + + if is_first_page && is_swipe_up { + return true; + } + + if is_last_page && is_swipe_down { + return true; + } + + false + } + + fn should_animate_swipe(&self, swipe_direction: SwipeDirection) -> bool { + let is_first_page = self.content.inner.current_page() == 0; + let is_last_page = + self.content.inner.current_page() == (self.content.inner.num_pages() - 1); + + let is_swipe_up = matches!(swipe_direction, SwipeDirection::Up); + let is_swipe_down = matches!(swipe_direction, SwipeDirection::Down); + + if is_last_page && is_swipe_up { + return true; + } + + if is_first_page && is_swipe_down { + return true; + } + + false + } +} + +impl Component for InternallySwipableContent +where + T: Component + InternallySwipable, +{ + type Msg = T::Msg; + + fn place(&mut self, bounds: Rect) -> Rect { + self.content.place(bounds) + } + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + let animate = match event { + Event::Attach(attach_type) => self.should_animate_attach(attach_type), + Event::Swipe(SwipeEvent::Move(dir, _)) => self.should_animate_swipe(dir), + Event::Swipe(SwipeEvent::End(dir)) => self.should_animate_swipe(dir), + _ => self.animate, + }; + + self.animate = animate; + + self.content.process_event(ctx, event, animate) + } + + fn paint(&mut self) { + self.content.paint() + } + + fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { + self.content.render(target) + } +} + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for InternallySwipableContent +where + T: crate::trace::Trace + Component + InternallySwipable, +{ + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("InternallySwipableContent"); + t.child("content", &self.content); + } +}