From 8906d8c4a140fcc9b7ef5dba7fde8982f99d497c Mon Sep 17 00:00:00 2001 From: Lukas Bielesch Date: Tue, 15 Apr 2025 11:58:19 +0200 Subject: [PATCH] chore(eckhart): enable vertical menu swipe without animations --- .../layout_eckhart/firmware/vertical_menu.rs | 55 +++++- .../firmware/vertical_menu_screen.rs | 167 +++++++++++------- 2 files changed, 156 insertions(+), 66 deletions(-) diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs index f992c38fac..78a0816654 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rs @@ -1,8 +1,9 @@ use crate::ui::{ component::{Component, Event, EventCtx}, event::TouchEvent, - geometry::{Insets, Offset, Rect}, + geometry::{Direction, Insets, Offset, Rect}, shape::{Bar, Renderer}, + util::animation_disabled, }; use super::{ @@ -90,7 +91,7 @@ impl VerticalMenu { /// Enable only buttons that are fully visible in the menu area. /// Meaningful only if the menu is scrollable. /// If the menu fits its area, all buttons are enabled. - pub fn update_menu(&mut self, ctx: &mut EventCtx) { + pub fn update_button_states(&mut self, ctx: &mut EventCtx) { for button in self.buttons.iter_mut() { let in_bounds = button .area() @@ -101,7 +102,57 @@ impl VerticalMenu { } } + /// Scroll the menu by one item in given direction. + /// Relevant only for testing purposes when the animations are disabled. + #[cfg(feature = "ui_debug")] + pub fn scroll_item(&mut self, dir: Direction) { + // Make sure the animations are disabled + debug_assert!(animation_disabled()); + // Only vertical swipes are allowed + debug_assert!(dir == Direction::Up || dir == Direction::Down); + + // For single button, the menu is not scrollable + if self.buttons.len() < 2 { + return; + } + + // The offset could reach only discrete values of cumsum of button heights + let current = self.offset_y; + let mut cumsum = 0; + + for button in &self.buttons[..self.buttons.len() - 1] { + let new_cumsum = cumsum + button.area().height(); + match dir { + Direction::Up if new_cumsum > current => { + self.set_offset(new_cumsum); + break; + } + Direction::Down if new_cumsum >= current => { + self.set_offset(cumsum); + break; + } + _ => { + cumsum = new_cumsum; + } + } + } + } + fn set_max_offset(&mut self) { + // Relevant only for testing when the animations are disabled + // The menu is scrollable until the last button is visible + #[cfg(feature = "ui_debug")] + if animation_disabled() { + self.offset_y_max = self.total_height + - self + .buttons + .last() + .unwrap_or(&Button::empty()) + .area() + .height(); + return; + } + // Calculate the overflow of the menu area let menu_overflow = (self.total_height - self.bounds.height()).max(0); diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rs index 9ad64054a7..60af49f206 100644 --- a/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rs +++ b/core/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rs @@ -9,7 +9,7 @@ use crate::{ flow::Swipable, geometry::{Alignment2D, Direction, Rect}, shape::{Renderer, ToifImage}, - util::Pager, + util::{animation_disabled, Pager}, }, }; @@ -21,10 +21,8 @@ pub struct VerticalMenuScreen { menu: VerticalMenu, /// Base position of the menu sliding window to scroll around offset_base: i16, - /// Used to enable swipe detection only when the menu does not fit its area - swipe_enabled: bool, /// Swipe detector - swipe: SwipeDetect, + swipe: Option, /// Swipe configuration swipe_config: SwipeConfig, } @@ -44,8 +42,7 @@ impl VerticalMenuScreen { header: Header::new(TString::empty()), menu, offset_base: 0, - swipe_enabled: false, - swipe: SwipeDetect::new(), + swipe: None, swipe_config: SwipeConfig::new() .with_swipe(Direction::Up, SwipeSettings::default()) .with_swipe(Direction::Down, SwipeSettings::default()), @@ -59,20 +56,108 @@ impl VerticalMenuScreen { /// Update swipe detection and buttons state based on menu size pub fn initialize_screen(&mut self, ctx: &mut EventCtx) { - if !self.menu.fits_area() { - // Enable swipe - self.swipe_enabled = true; - self.swipe_config = SwipeConfig::new() - .with_swipe(Direction::Up, SwipeSettings::default()) - .with_swipe(Direction::Down, SwipeSettings::default()); + #[cfg(feature = "ui_debug")] + if animation_disabled() { + self.swipe = Some(SwipeDetect::new()); ctx.enable_swipe(); - + // Set default position for the sliding window + self.menu.set_offset(0); // Update the menu buttons state - self.menu.update_menu(ctx); + self.menu.update_button_states(ctx); + return; + } + + // Switch swiping on/off based on the menu fit + self.swipe = if !self.menu.fits_area() { + ctx.enable_swipe(); + Some(SwipeDetect::new()) } else { - // Disable swipe - self.swipe_enabled = false; ctx.disable_swipe(); + None + }; + + // Set default position for the sliding window + self.menu.set_offset(0); + // Update button states + self.menu.update_button_states(ctx); + } + + fn handle_swipe_event(&mut self, ctx: &mut EventCtx, event: Event) { + // Relevant only for testing when the animations are disabled + // The menu is scrollable until the last button is visible + #[cfg(feature = "ui_debug")] + if animation_disabled() { + // Handle swipes from the standalone swipe detector or ones coming from + // the flow. These two are mutually exclusive and should not be triggered at the + // same time. + let direction = self + .swipe + .as_mut() + .and_then(|swipe| swipe.event(ctx, event, self.swipe_config)) + .and_then(|e| match e { + SwipeEvent::End(dir @ (Direction::Up | Direction::Down)) => Some(dir), + _ => None, + }) + .or(match event { + Event::Swipe(SwipeEvent::End(dir @ (Direction::Up | Direction::Down))) => { + Some(dir) + } + _ => None, + }); + + if let Some(dir) = direction { + self.menu.scroll_item(dir); + self.menu.update_button_states(ctx); + ctx.request_paint(); + } + return; + } + + if let Some(swipe) = &mut self.swipe { + // Handle swipe events from the standalone swipe detector or ones coming from + // the flow. These two are mutually exclusive and should not be triggered at the + // same time. + let swipe_event = swipe.event(ctx, event, self.swipe_config).or(match event { + Event::Swipe(e) => Some(e), + _ => None, + }); + + match swipe_event { + Some(SwipeEvent::Start(_) | SwipeEvent::End(_)) => { + // Lock the base position to scroll around + self.offset_base = self.menu.get_offset(); + } + Some(SwipeEvent::Move(dir @ (Direction::Up | Direction::Down), delta)) => { + // Reduce swipe sensitivity + let delta = delta / Self::TOUCH_SENSITIVITY_DIVIDER; + + let offset = match dir { + Direction::Up => self.offset_base + delta, + Direction::Down => self.offset_base - delta, + _ => unreachable!(), // Already matched Up or Down + }; + + self.menu.set_offset(offset); + self.menu.update_button_states(ctx); + } + _ => {} + } + } + } + + fn render_overflow_arrow<'s>(&'s self, target: &mut impl Renderer<'s>) { + // Do not render the arrow if animations are disabled + #[cfg(feature = "ui_debug")] + if animation_disabled() { + return; + } + + // Render the down arrow if the menu overflows and can be scrolled further down + if self.swipe.is_some() && !self.menu.is_max_offset() { + ToifImage::new(SCREEN.bottom_center(), theme::ICON_CHEVRON_DOWN_MINI.toif) + .with_align(Alignment2D::BOTTOM_CENTER) + .with_fg(theme::GREY_LIGHT) + .render(target); } } } @@ -100,46 +185,6 @@ impl Component for VerticalMenuScreen { self.initialize_screen(ctx); } - // Handle swipe events if swipe is enabled (menu does not fit) - if self.swipe_enabled { - // Handle swipe events from the standalone swipe detector or ones coming from - // the flow. These two are mutually exclusive and should not be triggered at the - // same time. - let swipe_event = self - .swipe - .event(ctx, event, self.swipe_config) - .or(match event { - Event::Swipe(e) => Some(e), - _ => None, - }); - - match swipe_event { - Some(SwipeEvent::Start(_) | SwipeEvent::End(_)) => { - // Lock the base position to scroll around - self.offset_base = self.menu.get_offset(); - } - Some(SwipeEvent::Move(dir, delta)) => { - // Decrease the sensitivity of the swipe - let delta = delta / Self::TOUCH_SENSITIVITY_DIVIDER; - // Scroll the menu based on the swipe direction - match dir { - Direction::Up => { - self.menu.set_offset(self.offset_base + delta); - self.menu.update_menu(ctx); - return None; - } - Direction::Down => { - self.menu.set_offset(self.offset_base - delta); - self.menu.update_menu(ctx); - return None; - } - _ => {} - } - } - _ => {} - } - } - if let Some(msg) = self.header.event(ctx, event) { match msg { HeaderMsg::Cancelled => return Some(VerticalMenuScreenMsg::Close), @@ -152,20 +197,14 @@ impl Component for VerticalMenuScreen { return Some(VerticalMenuScreenMsg::Selected(i)); } + self.handle_swipe_event(ctx, event); None } fn render<'s>(&'s self, target: &mut impl Renderer<'s>) { self.header.render(target); self.menu.render(target); - - // Render the down arrow if the menu overflows and can be scrolled further down - if !self.menu.fits_area() && !self.menu.is_max_offset() { - ToifImage::new(SCREEN.bottom_center(), theme::ICON_CHEVRON_DOWN_MINI.toif) - .with_align(Alignment2D::BOTTOM_CENTER) - .with_fg(theme::GREY_LIGHT) - .render(target); - } + self.render_overflow_arrow(target); } }