fix(eckhart): improve haptics in scrollable menu
What changed, and why it matters
This commit is a user-experience polish change for the Trezor hardware wallet's touchscreen interface. It changes when the device vibrates (haptic feedback) in scrollable menus so that the vibration happens when the user lifts their finger and confirms a tap, rather than when they first touch the screen. This prevents confusing vibrations while the user is merely scrolling through a long menu. There is no security-relevant change here.
No security action needed. Treat as a normal UX improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors haptic feedback in the Eckhart UI layout. It replaces a boolean haptic flag on Button with a three-state HapticMode enum (OnPress, OnClick, Off). For scrollable vertical menus (VerticalMenuScreen), it now sets HapticMode::OnClick so the ButtonPress haptic effect plays on TouchEnd instead of TouchStart. Non-scrollable menus keep the default OnPress behavior. The change is purely about timing of tactile feedback and does not alter input handling, authentication, cryptography, or any trust boundary.
Changed components
core/embed/rust/src/ui/layout_eckhart/component/button.rscore/embed/rust/src/ui/layout_eckhart/component/mod.rscore/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu.rscore/embed/rust/src/ui/layout_eckhart/firmware/vertical_menu_screen.rsInspect captured patch +41 / −17
diff --git a/core/embed/rust/src/ui/layout_eckhart/component/button.rs b/core/embed/rust/src/ui/layout_eckhart/component/button.rs
index f92bc7e5..a5aa7d89 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component/button.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component/button.rs
@@ -32,6 +32,16 @@ enum RadiusOrGradient {
None,
}
+#[derive(Clone, Copy)]
+pub enum HapticMode {
+ /// Vibrate on TouchStart (default)
+ OnPress,
+ /// Vibrate only on confirmed click
+ OnClick,
+ /// No haptic feedback
+ Off,
+}
+
pub struct Button {
area: Rect,
touch_expand: Insets,
@@ -44,7 +54,7 @@ pub struct Button {
long_press: ShortDuration, // long press requires non-zero duration
long_press_danger: bool,
long_timer: Timer,
- haptic: bool,
+ haptic: HapticMode,
subtext_marquee: Option<Marquee>,
#[cfg(feature = "ui_debug")]
skip_test_visit: bool, // used by debuglink
@@ -87,7 +97,7 @@ impl Button {
long_press: ShortDuration::ZERO,
long_press_danger: false,
long_timer: Timer::new(),
- haptic: true,
+ haptic: HapticMode::OnPress,
subtext_marquee,
#[cfg(feature = "ui_debug")]
skip_test_visit: false,
@@ -264,11 +274,6 @@ impl Button {
self
}
- pub fn without_haptics(mut self) -> Self {
- self.haptic = false;
- self
- }
-
pub fn with_gradient(mut self, gradient: Gradient) -> Self {
self.radius_or_gradient = RadiusOrGradient::Gradient(gradient);
self
@@ -454,6 +459,10 @@ impl Button {
}
}
+ pub fn set_haptic_mode(&mut self, mode: HapticMode) {
+ self.haptic = mode;
+ }
+
pub fn render_background<'s>(
&self,
target: &mut impl Renderer<'s>,
@@ -714,7 +723,7 @@ impl Component for Button {
// Touch started in our area, transform to `Pressed` state.
if touch_area.contains(pos) {
#[cfg(feature = "haptic")]
- if self.haptic {
+ if matches!(self.haptic, HapticMode::OnPress) {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Pressed);
@@ -745,6 +754,10 @@ impl Component for Button {
}
State::Pressed if touch_area.contains(pos) => {
// Touch finished in our area, we got clicked.
+ #[cfg(feature = "haptic")]
+ if matches!(self.haptic, HapticMode::OnClick) {
+ play(HapticEffect::ButtonPress);
+ }
self.set(ctx, State::Initial);
return Some(ButtonMsg::Clicked);
}
@@ -784,7 +797,7 @@ impl Component for Button {
Event::Timer(_) if self.long_timer.expire(event) => {
if matches!(self.state, State::Pressed) {
#[cfg(feature = "haptic")]
- if self.haptic {
+ if !matches!(self.haptic, HapticMode::Off) {
play(HapticEffect::ButtonPress);
}
self.set(ctx, State::Initial);
diff --git a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs
index 6511fd2c..51a90b25 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs
@@ -4,7 +4,9 @@ mod fuel_gauge;
mod update_screen;
mod welcome_screen;
-pub use button::{Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, IconText};
+pub use button::{
+ Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, HapticMode, IconText,
+};
pub use error::ErrorScreen;
pub use fuel_gauge::FuelGauge;
pub use update_screen::UpdateScreen;
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 d6c6dfae..d74bec4b 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
@@ -12,7 +12,7 @@ use crate::{
};
use super::{
- super::component::{Button, ButtonMsg},
+ super::component::{Button, ButtonMsg, HapticMode},
theme,
};
use heapless::Vec;
@@ -139,6 +139,13 @@ impl<T: MenuItems> VerticalMenu<T> {
self
}
+ /// Set haptic mode for all buttons in the menu.
+ pub fn set_haptic_mode(&mut self, mode: HapticMode) {
+ for button in self.buttons.iter_mut() {
+ button.set_haptic_mode(mode);
+ }
+ }
+
pub fn item(&mut self, button: Button) -> &mut Self {
self.buttons.push(button);
self
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 5e4fcee9..7fa15d83 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
@@ -16,8 +16,8 @@ use crate::{
};
use super::{
- constant::SCREEN, theme, Header, HeaderMsg, MenuItems, ShortMenuVec, VerticalMenu,
- VerticalMenuMsg,
+ super::component::HapticMode, constant::SCREEN, theme, Header, HeaderMsg, MenuItems,
+ ShortMenuVec, VerticalMenu, VerticalMenuMsg,
};
pub struct VerticalMenuScreen<T> {
@@ -95,13 +95,15 @@ impl<T: MenuItems> VerticalMenuScreen<T> {
}
// Switch swiping on/off based on the menu fit
- self.swipe = if !self.menu.fits_area() {
+ if !self.menu.fits_area() {
ctx.enable_swipe();
- Some(SwipeDetect::new())
+ self.swipe = Some(SwipeDetect::new());
+ // Delay haptic feedback to click for scrollable menus
+ self.menu.set_haptic_mode(HapticMode::OnClick);
} else {
ctx.disable_swipe();
- None
- };
+ self.swipe = None;
+ }
// Set default position for the sliding window
self.menu.set_offset(0);
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.