diff --git a/core/embed/rust/src/ui/model_tr/component/button_controller.rs b/core/embed/rust/src/ui/model_tr/component/button_controller.rs index 76d053faa..545bfe766 100644 --- a/core/embed/rust/src/ui/model_tr/component/button_controller.rs +++ b/core/embed/rust/src/ui/model_tr/component/button_controller.rs @@ -394,6 +394,13 @@ impl Component for ButtonController { // State machine for the ButtonController // We are matching event with `Event::Button` for a button action // and `Event::Timer` for getting the expiration of HTC. + + if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { + // Filter out opposite buttons after some delay + if ignore_btn_delay.filter_event(event) { + return None; + } + } match event { Event::Button(button_event) => { let (new_state, event) = match self.state { @@ -404,9 +411,6 @@ impl Component for ButtonController { // ↓ _ | _ ↓ // Initial button press will set the timer for second button, // and after some delay, it will become un-clickable - if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { - ignore_btn_delay.handle_button_press(ctx, which); - } ( ButtonState::OneDown(which), match which { @@ -432,33 +436,16 @@ impl Component for ButtonController { // ▲ * | * ▲ ButtonEvent::ButtonReleased(b) if b == which_down => match which_down { // ▲ * - // Trigger the button and make the second one clickable in all cases PhysicalButton::Left => { - if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { - ignore_btn_delay.make_button_clickable(ButtonPos::Right); - } - // _ _ (ButtonState::Nothing, self.left_btn.maybe_trigger(ctx)) } // * ▲ PhysicalButton::Right => { - if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { - ignore_btn_delay.make_button_clickable(ButtonPos::Left); - } - // _ _ (ButtonState::Nothing, self.right_btn.maybe_trigger(ctx)) } }, // * ▼ | ▼ * ButtonEvent::ButtonPressed(b) if b != which_down => { - // Buttons may be non-clickable (caused by long-holding the other - // button) - if let Some(ignore_btn_delay) = &self.ignore_btn_delay { - if ignore_btn_delay.ignore_button(b) { - return None; - } - } - // ↓ ↓ if self.handle_middle_button { self.got_pressed(ctx, ButtonPos::Middle); self.middle_hold_started(ctx); @@ -505,10 +492,7 @@ impl Component for ButtonController { ButtonEvent::ButtonReleased(b) if b != which_up => { // _ _ // Both buttons need to be clickable now - if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { - ignore_btn_delay.make_button_clickable(ButtonPos::Left); - ignore_btn_delay.make_button_clickable(ButtonPos::Right); - } + self.ignore_btn_delay.as_mut().map(IgnoreButtonDelay::reset); if self.handle_middle_button { (ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx)) } else { @@ -556,9 +540,6 @@ impl Component for ButtonController { } // Timer - handle clickable properties and HoldToConfirm expiration Event::Timer(token) => { - if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { - ignore_btn_delay.handle_timer_token(token); - } if let Some(pos) = self.handle_long_press_timer_token(token) { return Some(ButtonControllerMsg::LongPressed(pos)); } @@ -589,82 +570,110 @@ impl Component for ButtonController { } } +#[derive(Copy, Clone)] +enum IgnoreButtonDelayState { + // All buttons are clickable. + Unlocked, + // Button was pressed down at a given time. + ButtonDown(PhysicalButton, Instant), + // Button is locked, events for all other buttons are ignored. + ButtonLocked(PhysicalButton), +} + /// When one button is pressed, the other one becomes un-clickable after some /// small time period. This is to prevent accidental clicks when user is holding /// the button to automatically move through items. struct IgnoreButtonDelay { /// How big is the delay after the button is inactive delay: Duration, - /// Whether left button is currently clickable - left_clickable: bool, - /// Whether right button is currently clickable - right_clickable: bool, - /// Timer for setting the left_clickable - left_clickable_timer: Option, - /// Timer for setting the right_clickable - right_clickable_timer: Option, + /// Lock state + state: IgnoreButtonDelayState, + /// State of an ignored button that was pressed down while another button + /// was locked. We need to filter out its release event even after the + /// lock expires, otherwise a spurious Up event with no corresponding + /// Down event will escape. + ignored_button_needs_release: Option, } impl IgnoreButtonDelay { pub fn new(delay_ms: u32) -> Self { Self { delay: Duration::from_millis(delay_ms), - left_clickable: true, - right_clickable: true, - left_clickable_timer: None, - right_clickable_timer: None, + state: IgnoreButtonDelayState::Unlocked, + ignored_button_needs_release: None, } } - pub fn make_button_clickable(&mut self, pos: ButtonPos) { - match pos { - ButtonPos::Left => { - self.left_clickable = true; - self.left_clickable_timer = None; + /// Filter a ButtonPressed event. + fn filter_pressed(&mut self, button: PhysicalButton) -> bool { + match self.state { + IgnoreButtonDelayState::Unlocked => { + // A new button was pressed. Register ButtonDown, do not filter. + self.state = IgnoreButtonDelayState::ButtonDown(button, Instant::now()); + false } - ButtonPos::Right => { - self.right_clickable = true; - self.right_clickable_timer = None; + IgnoreButtonDelayState::ButtonDown(b, ..) if b != button => { + // A different button was pressed before locking the first one. + // Unlock, do not filter. + self.state = IgnoreButtonDelayState::Unlocked; + false } - ButtonPos::Middle => {} - } - } - - pub fn handle_button_press(&mut self, ctx: &mut EventCtx, button: PhysicalButton) { - if matches!(button, PhysicalButton::Left) { - self.right_clickable_timer = Some(ctx.request_timer(self.delay)); - } - if matches!(button, PhysicalButton::Right) { - self.left_clickable_timer = Some(ctx.request_timer(self.delay)); + IgnoreButtonDelayState::ButtonLocked(b) if b != button => { + // A button is already locked, and another button was pressed. + // Track that this new button needs release, and filter the event. + self.ignored_button_needs_release = Some(button); + true + } + // Allow all other events. + _ => false, } } - pub fn ignore_button(&self, button: PhysicalButton) -> bool { - if matches!(button, PhysicalButton::Left) && !self.left_clickable { - return true; - } - if matches!(button, PhysicalButton::Right) && !self.right_clickable { + /// Filter a ButtonReleased event. + fn filter_released(&mut self, button: PhysicalButton) -> bool { + if matches!(self.ignored_button_needs_release, Some(b) if b == button) { + // Regardless of lock state, the ignored button that was previously pressed + // is now released. Clear the needs_release state and filter the event. + self.ignored_button_needs_release = None; return true; } + match self.state { + IgnoreButtonDelayState::ButtonDown(b, ..) if b == button => { + // The button which was down was released. Unlock. + self.state = IgnoreButtonDelayState::Unlocked; + } + IgnoreButtonDelayState::ButtonLocked(b) if b == button => { + // The locked button was released. Unlock. + self.state = IgnoreButtonDelayState::Unlocked; + } + _ => {} + }; false } - pub fn handle_timer_token(&mut self, token: TimerToken) { - if self.left_clickable_timer == Some(token) { - self.left_clickable = false; - self.left_clickable_timer = None; + /// Filter an incoming event + /// + /// If the return value is `true`, the event is filtered and should not be + /// processed further. Otherwise, the event should be processed as usual. + pub fn filter_event(&mut self, event: Event) -> bool { + // resolve delay elapsed event first + if let IgnoreButtonDelayState::ButtonDown(button, last_ms) = self.state { + let elapsed = Instant::now().saturating_duration_since(last_ms); + if elapsed >= self.delay { + self.state = IgnoreButtonDelayState::ButtonLocked(button); + } } - if self.right_clickable_timer == Some(token) { - self.right_clickable = false; - self.right_clickable_timer = None; + + match event { + Event::Button(ButtonEvent::ButtonPressed(button)) => self.filter_pressed(button), + Event::Button(ButtonEvent::ButtonReleased(button)) => self.filter_released(button), + _ => false, } } pub fn reset(&mut self) { - self.left_clickable = true; - self.right_clickable = true; - self.left_clickable_timer = None; - self.right_clickable_timer = None; + self.state = IgnoreButtonDelayState::Unlocked; + self.ignored_button_needs_release = None; } }