1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-05-30 12:48:46 +00:00

core(eckhart): Update Vertical Menu

Leave event processing to the Vertical Menu
Disable scrolling when the menu fits the screen
This commit is contained in:
Lukas Bielesch 2025-04-11 16:24:38 +02:00 committed by obrusvit
parent 3ff6b55520
commit 941875c254
5 changed files with 425 additions and 403 deletions

View File

@ -421,10 +421,13 @@ impl<'a> DeviceMenuScreen<'a> {
Subscreen::Submenu(ref mut submenu_index) => { Subscreen::Submenu(ref mut submenu_index) => {
match self.submenus[*submenu_index].items[idx].action { match self.submenus[*submenu_index].items[idx].action {
Some(Action::GoTo(menu)) => { Some(Action::GoTo(menu)) => {
self.menu_screen.as_mut().unwrap().update_menu(ctx);
unwrap!(self.parent_subscreens.push(self.active_subscreen)); unwrap!(self.parent_subscreens.push(self.active_subscreen));
self.set_active_subscreen(menu); self.set_active_subscreen(menu);
self.place(self.bounds); self.place(self.bounds);
if let Some(screen) = self.menu_screen.as_mut() {
screen.initialize_screen(ctx);
}
return None;
} }
Some(Action::Return(msg)) => return Some(msg), Some(Action::Return(msg)) => return Some(msg),
None => {} None => {}
@ -438,10 +441,13 @@ impl<'a> DeviceMenuScreen<'a> {
None None
} }
fn go_back(&mut self) -> Option<DeviceMenuMsg> { fn go_back(&mut self, ctx: &mut EventCtx) -> Option<DeviceMenuMsg> {
if let Some(parent) = self.parent_subscreens.pop() { if let Some(parent) = self.parent_subscreens.pop() {
self.set_active_subscreen(parent); self.set_active_subscreen(parent);
self.place(self.bounds); self.place(self.bounds);
if let Some(screen) = self.menu_screen.as_mut() {
screen.initialize_screen(ctx);
}
None None
} else { } else {
Some(DeviceMenuMsg::Close) Some(DeviceMenuMsg::Close)
@ -483,7 +489,7 @@ impl<'a> Component for DeviceMenuScreen<'a> {
} }
} }
Some(VerticalMenuScreenMsg::Back) => { Some(VerticalMenuScreenMsg::Back) => {
return self.go_back(); return self.go_back(ctx);
} }
Some(VerticalMenuScreenMsg::Close) => { Some(VerticalMenuScreenMsg::Close) => {
return Some(DeviceMenuMsg::Close); return Some(DeviceMenuMsg::Close);
@ -493,7 +499,7 @@ impl<'a> Component for DeviceMenuScreen<'a> {
} }
Subscreen::AboutScreen => { Subscreen::AboutScreen => {
if let Some(TextScreenMsg::Cancelled) = self.about_screen.event(ctx, event) { if let Some(TextScreenMsg::Cancelled) = self.about_screen.event(ctx, event) {
return self.go_back(); return self.go_back(ctx);
} }
} }
} }

View File

@ -36,7 +36,7 @@ impl SelectWordScreen {
share_words_vec: [TString<'static>; MAX_WORD_QUIZ_ITEMS], share_words_vec: [TString<'static>; MAX_WORD_QUIZ_ITEMS],
description: TString<'static>, description: TString<'static>,
) -> Self { ) -> Self {
let mut menu = VerticalMenu::empty().with_separators().with_fit_area(); let mut menu = VerticalMenu::empty().with_separators().with_content_fit();
for word in share_words_vec { for word in share_words_vec {
menu = menu.item( menu = menu.item(

View File

@ -1,5 +1,6 @@
use crate::ui::{ use crate::ui::{
component::{Component, Event, EventCtx}, component::{Component, Event, EventCtx},
event::TouchEvent,
geometry::{Insets, Offset, Rect}, geometry::{Insets, Offset, Rect},
shape::{Bar, Renderer}, shape::{Bar, Renderer},
}; };
@ -19,19 +20,19 @@ type VerticalMenuButtons = Vec<Button, MENU_MAX_ITEMS>;
pub struct VerticalMenu { pub struct VerticalMenu {
/// Bounds the sliding window of the menu. /// Bounds the sliding window of the menu.
bounds: Rect, bounds: Rect,
/// Full bounds of the menu, including off-screen items.
virtual_bounds: Rect,
/// Menu items. /// Menu items.
buttons: VerticalMenuButtons, buttons: VerticalMenuButtons,
/// Whether to show separators between buttons. /// Full height of the menu, including overflowing items.
separators: bool, total_height: i16,
/// Vertical offset of the current view. /// Vertical offset of the current view.
offset_y: i16, offset_y: i16,
/// Maximum vertical offset. /// Maximum vertical offset.
max_offset: i16, offset_y_max: i16,
/// Adapt padding to fit entire area. If the area is too small, the padding /// Adapt padding to fit entire area. If the area is too small, the padding
/// will be reduced to min value. /// will be reduced to min value.
fit_area: bool, content_fit: bool,
/// Whether to show separators between buttons.
separators: bool,
} }
pub enum VerticalMenuMsg { pub enum VerticalMenuMsg {
@ -45,13 +46,13 @@ impl VerticalMenu {
fn new(buttons: VerticalMenuButtons) -> Self { fn new(buttons: VerticalMenuButtons) -> Self {
Self { Self {
virtual_bounds: Rect::zero(),
bounds: Rect::zero(), bounds: Rect::zero(),
buttons, buttons,
separators: false, total_height: 0,
offset_y: 0, offset_y: 0,
max_offset: 0, offset_y_max: 0,
fit_area: false, separators: false,
content_fit: false,
} }
} }
@ -64,8 +65,8 @@ impl VerticalMenu {
self self
} }
pub fn with_fit_area(mut self) -> Self { pub fn with_content_fit(mut self) -> Self {
self.fit_area = true; self.content_fit = true;
self self
} }
@ -74,18 +75,19 @@ impl VerticalMenu {
self self
} }
pub fn area(&self) -> Rect { /// Check if the menu fits its area without scrolling.
self.bounds pub fn fits_area(&self) -> bool {
self.total_height <= self.bounds.height()
} }
/// Scroll the menu to the desired offset. /// Scroll the menu to the desired offset.
pub fn set_offset(&mut self, offset_y: i16) { pub fn set_offset(&mut self, offset_y: i16) {
self.offset_y = offset_y.max(0).min(self.max_offset); self.offset_y = offset_y.max(0).min(self.offset_y_max);
} }
/// Chcek if the menu is on the bottom. /// Chcek if the menu is on the bottom.
pub fn is_max_offset(&self) -> bool { pub fn is_max_offset(&self) -> bool {
self.offset_y == self.max_offset self.offset_y == self.offset_y_max
} }
/// Get the current sliding window offset. /// Get the current sliding window offset.
@ -93,7 +95,10 @@ impl VerticalMenu {
self.offset_y self.offset_y
} }
/// Update menu buttons based on the current offset. /// Update state of menu buttons based on the current offset.
/// 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_menu(&mut self, ctx: &mut EventCtx) {
for button in self.buttons.iter_mut() { for button in self.buttons.iter_mut() {
let in_bounds = button let in_bounds = button
@ -107,19 +112,35 @@ impl VerticalMenu {
fn set_max_offset(&mut self) { fn set_max_offset(&mut self) {
// Calculate the overflow of the menu area // Calculate the overflow of the menu area
let menu_overflow = (self.virtual_bounds.height() - self.bounds.height()).max(0); let menu_overflow = (self.total_height - self.bounds.height()).max(0);
// Find the first button from the top that would completely fit in the menu area // Find the first button from the top that would completely fit in the menu area
// in the bottom position // in the bottom position
for button in &self.buttons { for button in &self.buttons {
let offset = button.area().top_left().y - self.area().top_left().y; let offset = button.area().top_left().y - self.bounds.top_left().y;
if offset > menu_overflow { if offset > menu_overflow {
self.max_offset = offset; self.offset_y_max = offset;
return; return;
} }
} }
self.max_offset = menu_overflow; self.offset_y_max = menu_overflow;
}
// Shift position of touch events in the menu area by an offset of the current
// sliding window position
fn shift_touch_event(&self, event: Event) -> Event {
match event {
Event::Touch(t) => {
let o = Offset::y(self.offset_y);
Event::Touch(match t {
TouchEvent::TouchStart(p) => TouchEvent::TouchStart(p.ofs(o)),
TouchEvent::TouchMove(p) => TouchEvent::TouchMove(p.ofs(o)),
TouchEvent::TouchEnd(p) => TouchEvent::TouchEnd(p.ofs(o)),
})
}
_ => event,
}
} }
fn render_buttons<'s>(&'s self, target: &mut impl Renderer<'s>) { fn render_buttons<'s>(&'s self, target: &mut impl Renderer<'s>) {
@ -156,8 +177,8 @@ impl Component for VerticalMenu {
// Crop the menu area // Crop the menu area
self.bounds = bounds.inset(Self::SIDE_INSETS); self.bounds = bounds.inset(Self::SIDE_INSETS);
// Determine padding dynamically if `fit_area` is enabled // Determine padding dynamically if `content_fit` is enabled
let padding = if self.fit_area { let padding = if self.content_fit {
let mut content_height = 0; let mut content_height = 0;
for button in self.buttons.iter_mut() { for button in self.buttons.iter_mut() {
content_height += button.content_height(); content_height += button.content_height();
@ -182,12 +203,8 @@ impl Component for VerticalMenu {
top_left = top_left + Offset::y(button_height); top_left = top_left + Offset::y(button_height);
} }
// Calculate virtual bounds of all buttons combined // Calculate height of all buttons combined
let total_height = top_left.y - self.bounds.top_left().y; self.total_height = top_left.y - self.bounds.top_left().y;
self.virtual_bounds = Rect::from_top_left_and_size(
self.bounds.top_left(),
Offset::new(self.bounds.width(), total_height),
);
// Calculate maximum offset for scrolling // Calculate maximum offset for scrolling
self.set_max_offset(); self.set_max_offset();
@ -196,8 +213,10 @@ impl Component for VerticalMenu {
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Shif touch events by the scroll offset
let event_shifted = self.shift_touch_event(event);
for (i, button) in self.buttons.iter_mut().enumerate() { for (i, button) in self.buttons.iter_mut().enumerate() {
if let Some(ButtonMsg::Clicked) = button.event(ctx, event) { if let Some(ButtonMsg::Clicked) = button.event(ctx, event_shifted) {
return Some(VerticalMenuMsg::Selected(i)); return Some(VerticalMenuMsg::Selected(i));
} }
} }

View File

@ -2,13 +2,12 @@ use crate::{
strutil::TString, strutil::TString,
ui::{ ui::{
component::{ component::{
base::AttachType,
swipe_detect::{SwipeConfig, SwipeSettings}, swipe_detect::{SwipeConfig, SwipeSettings},
Component, Event, EventCtx, SwipeDetect, Component, Event, EventCtx, SwipeDetect,
}, },
event::{SwipeEvent, TouchEvent}, event::SwipeEvent,
flow::Swipable, flow::Swipable,
geometry::{Alignment2D, Direction, Offset, Rect}, geometry::{Alignment2D, Direction, Rect},
shape::{Renderer, ToifImage}, shape::{Renderer, ToifImage},
util::Pager, util::Pager,
}, },
@ -22,6 +21,8 @@ pub struct VerticalMenuScreen {
menu: VerticalMenu, menu: VerticalMenu,
/// Base position of the menu sliding window to scroll around /// Base position of the menu sliding window to scroll around
offset_base: i16, offset_base: i16,
/// Used to enable swipe detection only when the menu does not fit its area
swipe_enabled: bool,
/// Swipe detector /// Swipe detector
swipe: SwipeDetect, swipe: SwipeDetect,
/// Swipe configuration /// Swipe configuration
@ -37,11 +38,13 @@ pub enum VerticalMenuScreenMsg {
} }
impl VerticalMenuScreen { impl VerticalMenuScreen {
const TOUCH_SENSITIVITY_DIVIDER: i16 = 15;
pub fn new(menu: VerticalMenu) -> Self { pub fn new(menu: VerticalMenu) -> Self {
Self { Self {
header: Header::new(TString::empty()), header: Header::new(TString::empty()),
menu, menu,
offset_base: 0, offset_base: 0,
swipe_enabled: false,
swipe: SwipeDetect::new(), swipe: SwipeDetect::new(),
swipe_config: SwipeConfig::new() swipe_config: SwipeConfig::new()
.with_swipe(Direction::Up, SwipeSettings::default()) .with_swipe(Direction::Up, SwipeSettings::default())
@ -54,32 +57,23 @@ impl VerticalMenuScreen {
self self
} }
// Shift position of touch events in the menu area by an offset of the current /// Update swipe detection and buttons state based on menu size
// sliding window position pub fn initialize_screen(&mut self, ctx: &mut EventCtx) {
fn shift_touch_event(&self, event: Event) -> Option<Event> { if !self.menu.fits_area() {
match event { // Enable swipe
Event::Touch(touch_event) => { self.swipe_enabled = true;
let shifted_event = match touch_event { self.swipe_config = SwipeConfig::new()
TouchEvent::TouchStart(point) if self.menu.area().contains(point) => Some( .with_swipe(Direction::Up, SwipeSettings::default())
TouchEvent::TouchStart(point.ofs(Offset::y(self.menu.get_offset()))), .with_swipe(Direction::Down, SwipeSettings::default());
), ctx.enable_swipe();
TouchEvent::TouchMove(point) if self.menu.area().contains(point) => Some(
TouchEvent::TouchMove(point.ofs(Offset::y(self.menu.get_offset()))),
),
TouchEvent::TouchEnd(point) if self.menu.area().contains(point) => Some(
TouchEvent::TouchEnd(point.ofs(Offset::y(self.menu.get_offset()))),
),
_ => None, // Ignore touch events outside the bounds
};
shifted_event.map(Event::Touch)
}
_ => None, // Ignore other events
}
}
/// Update menu buttons based on the current offset. // Update the menu buttons state
pub fn update_menu(&mut self, ctx: &mut EventCtx) { self.menu.update_menu(ctx);
self.menu.update_menu(ctx); } else {
// Disable swipe
self.swipe_enabled = false;
ctx.disable_swipe();
}
} }
} }
@ -93,49 +87,58 @@ impl Component for VerticalMenuScreen {
let (header_area, menu_area) = bounds.split_top(Header::HEADER_HEIGHT); let (header_area, menu_area) = bounds.split_top(Header::HEADER_HEIGHT);
self.menu.place(menu_area);
self.header.place(header_area); self.header.place(header_area);
self.menu.place(menu_area);
bounds bounds
} }
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> { fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Update the menu when the screen is attached // Update the screen after the menu fit is calculated
if let Event::Attach(AttachType::Initial) = event { // This is needed to enable swipe detection only when the menu does not fit
self.update_menu(ctx); if let Event::Attach(_) = event {
self.initialize_screen(ctx);
} }
match self.swipe.event(ctx, event, self.swipe_config) { // Handle swipe events if swipe is enabled (menu does not fit)
Some(SwipeEvent::Start(_)) => { if self.swipe_enabled {
// Lock the base position to scroll around // Handle swipe events from the standalone swipe detector or ones coming from
self.offset_base = self.menu.get_offset(); // 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,
});
Some(SwipeEvent::End(_)) => { match swipe_event {
// Lock the base position to scroll around Some(SwipeEvent::Start(_) | SwipeEvent::End(_)) => {
self.offset_base = self.menu.get_offset(); // 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 / 10;
// 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;
}
_ => {}
} }
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) { if let Some(msg) = self.header.event(ctx, event) {
match msg { match msg {
@ -145,11 +148,8 @@ impl Component for VerticalMenuScreen {
} }
} }
// Shift touch events in the menu area by the current sliding window position if let Some(VerticalMenuMsg::Selected(i)) = self.menu.event(ctx, event) {
if let Some(shifted) = self.shift_touch_event(event) { return Some(VerticalMenuScreenMsg::Selected(i));
if let Some(VerticalMenuMsg::Selected(i)) = self.menu.event(ctx, shifted) {
return Some(VerticalMenuScreenMsg::Selected(i));
}
} }
None None
@ -159,15 +159,12 @@ impl Component for VerticalMenuScreen {
self.header.render(target); self.header.render(target);
self.menu.render(target); self.menu.render(target);
// Render the down arrow if the menu can be scrolled down // Render the down arrow if the menu overflows and can be scrolled further down
if !self.menu.is_max_offset() { if !self.menu.fits_area() && !self.menu.is_max_offset() {
ToifImage::new( ToifImage::new(SCREEN.bottom_center(), theme::ICON_CHEVRON_DOWN_MINI.toif)
self.menu.area().bottom_center(), .with_align(Alignment2D::BOTTOM_CENTER)
theme::ICON_CHEVRON_DOWN_MINI.toif, .with_fg(theme::GREY_LIGHT)
) .render(target);
.with_align(Alignment2D::BOTTOM_CENTER)
.with_fg(theme::GREY_LIGHT)
.render(target);
} }
} }
} }

File diff suppressed because it is too large Load Diff