diff --git a/core/embed/rust/src/ui/model_tr/component/button.rs b/core/embed/rust/src/ui/model_tr/component/button.rs index 1746ccf7de..0c91b07ee9 100644 --- a/core/embed/rust/src/ui/model_tr/component/button.rs +++ b/core/embed/rust/src/ui/model_tr/component/button.rs @@ -13,7 +13,7 @@ use super::{loader::DEFAULT_DURATION_MS, theme}; const HALF_SCREEN_BUTTON_WIDTH: i16 = constant::WIDTH / 2 - 1; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Eq, PartialEq)] pub enum ButtonPos { Left, Middle, 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 638b1284cd..618b4e6742 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 @@ -3,8 +3,9 @@ use super::{ }; use crate::{ strutil::StringType, + time::Duration, ui::{ - component::{base::Event, Component, EventCtx, Pad}, + component::{base::Event, Component, EventCtx, Pad, TimerToken}, event::{ButtonEvent, PhysicalButton}, geometry::Rect, }, @@ -195,6 +196,9 @@ where /// /// Only "final" button events are returned in `ButtonControllerMsg::Triggered`, /// based upon the buttons being long-press or not. +/// +/// There is optional complexity with IgnoreButtonDelay, which gets executed +/// only in cases where clients request it. pub struct ButtonController where T: StringType, @@ -204,10 +208,12 @@ where middle_btn: ButtonContainer, right_btn: ButtonContainer, state: ButtonState, - // Button area is needed so the buttons - // can be "re-placed" after their text is changed - // Will be set in `place` + /// Button area is needed so the buttons + /// can be "re-placed" after their text is changed + /// Will be set in `place` button_area: Rect, + /// Handling optional ignoring of buttons after pressing the other button. + ignore_btn_delay: Option, } impl ButtonController @@ -222,9 +228,17 @@ where right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right), state: ButtonState::Nothing, button_area: Rect::zero(), + ignore_btn_delay: None, } } + /// Set up the logic to ignore a button after some time from pressing + /// the other button. + pub fn with_ignore_btn_delay(mut self, delay_ms: u32) -> Self { + self.ignore_btn_delay = Some(IgnoreButtonDelay::new(delay_ms)); + self + } + /// Updating all the three buttons to the wanted states. pub fn set(&mut self, btn_layout: ButtonLayout) { self.pad.clear(); @@ -240,6 +254,14 @@ where self.right_btn.set_pressed(ctx, right); } + pub fn highlight_button(&mut self, ctx: &mut EventCtx, pos: ButtonPos) { + match pos { + ButtonPos::Left => self.left_btn.set_pressed(ctx, true), + ButtonPos::Middle => self.middle_btn.set_pressed(ctx, true), + ButtonPos::Right => self.right_btn.set_pressed(ctx, true), + } + } + /// Handle middle button hold-to-confirm start. /// We need to cancel possible holds in both other buttons. fn middle_hold_started(&mut self, ctx: &mut EventCtx) { @@ -291,22 +313,29 @@ where // _ _ ButtonState::Nothing => match button_event { // ▼ * | * ▼ - ButtonEvent::ButtonPressed(which) => ( + ButtonEvent::ButtonPressed(which) => { // ↓ _ | _ ↓ - ButtonState::OneDown(which), - match which { - // ▼ * - PhysicalButton::Left => { - self.left_btn.hold_started(ctx); - Some(ButtonControllerMsg::Pressed(ButtonPos::Left)) - } - // * ▼ - PhysicalButton::Right => { - self.right_btn.hold_started(ctx); - Some(ButtonControllerMsg::Pressed(ButtonPos::Right)) - } - }, - ), + // 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 { + // ▼ * + PhysicalButton::Left => { + self.left_btn.hold_started(ctx); + Some(ButtonControllerMsg::Pressed(ButtonPos::Left)) + } + // * ▼ + PhysicalButton::Right => { + self.right_btn.hold_started(ctx); + Some(ButtonControllerMsg::Pressed(ButtonPos::Right)) + } + }, + ) + } _ => (self.state, None), }, // ↓ _ | _ ↓ @@ -314,18 +343,32 @@ where // ▲ * | * ▲ 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; + } + } self.middle_hold_started(ctx); ( // ↓ ↓ @@ -356,6 +399,11 @@ where // ▲ * | * ▲ ButtonEvent::ButtonReleased(b) if b != which_up => { // _ _ + // Both button needs 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); + } (ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx)) } _ => (self.state, None), @@ -394,8 +442,13 @@ where self.state = new_state; event } - // HoldToConfirm expiration - Event::Timer(_) => self.handle_htc_expiration(ctx, event), + // 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); + } + self.handle_htc_expiration(ctx, event) + } _ => None, } } @@ -421,6 +474,179 @@ where } } +/// 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, +} + +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, + } + } + + pub fn make_button_clickable(&mut self, pos: ButtonPos) { + match pos { + ButtonPos::Left => { + self.left_clickable = true; + self.left_clickable_timer = None; + } + ButtonPos::Right => { + self.right_clickable = true; + self.right_clickable_timer = None; + } + 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)); + } + } + + 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 { + return true; + } + 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; + } + if self.right_clickable_timer == Some(token) { + self.right_clickable = false; + self.right_clickable_timer = None; + } + } +} + +/// Component allowing for automatically moving through items (e.g. Choice +/// items). +/// +/// Users are in full control of starting/stopping the movement. +/// +/// Can be started e.g. by holding left/right button. +pub struct AutomaticMover { + /// For requesting timer events repeatedly + timer_token: Option, + /// Which direction should we go (which button is down) + moving_direction: Option, + /// How many screens were moved automatically + auto_moved_screens: usize, + /// Function to get duration of each movement according to the already moved + /// steps + duration_func: fn(usize) -> u32, +} + +impl AutomaticMover { + pub fn new() -> Self { + fn default_duration_func(steps: usize) -> u32 { + match steps { + x if x < 3 => 200, + x if x < 10 => 150, + _ => 100, + } + } + + Self { + timer_token: None, + moving_direction: None, + auto_moved_screens: 0, + duration_func: default_duration_func, + } + } + + pub fn with_duration_func(mut self, duration_func: fn(usize) -> u32) -> Self { + self.duration_func = duration_func; + self + } + + /// Determines how long to wait between automatic movements. + /// Moves quicker with increasing number of screens moved. + /// Can be forced to be always the same (e.g. for animation purposes). + fn get_auto_move_duration(&self) -> Duration { + // Calculating duration from function + let ms_duration = (self.duration_func)(self.auto_moved_screens); + Duration::from_millis(ms_duration) + } + + /// In which direction we are moving, if any + pub fn moving_direction(&self) -> Option { + self.moving_direction + } + + // Whether we are currently moving. + pub fn is_moving(&self) -> bool { + self.moving_direction.is_some() + } + + /// Whether we have done at least one automatic movement. + pub fn was_moving(&self) -> bool { + self.auto_moved_screens > 0 + } + + pub fn start_moving(&mut self, ctx: &mut EventCtx, button: ButtonPos) { + self.auto_moved_screens = 0; + self.moving_direction = Some(button); + self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration())); + } + + pub fn stop_moving(&mut self) { + self.moving_direction = None; + self.timer_token = None; + } +} + +impl Component for AutomaticMover { + type Msg = ButtonPos; + + fn place(&mut self, bounds: Rect) -> Rect { + bounds + } + + fn paint(&mut self) {} + + fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // Moving automatically only when we receive a TimerToken that we have + // requested before + if let Event::Timer(token) = event { + if self.timer_token == Some(token) && self.moving_direction.is_some() { + // Request new token and send the appropriate button trigger event + self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration())); + self.auto_moved_screens += 1; + return self.moving_direction; + } + } + None + } +} + // DEBUG-ONLY SECTION BELOW #[cfg(feature = "ui_debug")] @@ -443,3 +669,10 @@ impl crate::trace::Trace for ButtonController { t.child("right_btn", &self.right_btn); } } + +#[cfg(feature = "ui_debug")] +impl crate::trace::Trace for AutomaticMover { + fn trace(&self, t: &mut dyn crate::trace::Tracer) { + t.component("AutomaticMover"); + } +} diff --git a/core/embed/rust/src/ui/model_tr/component/flow.rs b/core/embed/rust/src/ui/model_tr/component/flow.rs index 3a4d8956fe..25dad78594 100644 --- a/core/embed/rust/src/ui/model_tr/component/flow.rs +++ b/core/embed/rust/src/ui/model_tr/component/flow.rs @@ -30,6 +30,9 @@ where page_counter: usize, return_confirmed_index: bool, show_scrollbar: bool, + /// Possibly enforcing the second button to be ignored after some time after + /// pressing the first button + ignore_second_button_ms: Option, } impl Flow @@ -55,6 +58,7 @@ where page_counter: 0, return_confirmed_index: false, show_scrollbar: true, + ignore_second_button_ms: None, } } @@ -77,6 +81,12 @@ where self } + /// Ignoring the second button duration + pub fn with_ignore_second_button_ms(mut self, ignore_second_button_ms: u32) -> Self { + self.ignore_second_button_ms = Some(ignore_second_button_ms); + self + } + pub fn confirmed_index(&self) -> Option { self.return_confirmed_index.then_some(self.page_counter) } @@ -225,7 +235,14 @@ where // We finally found how long is the first page, and can set its button layout. self.current_page.place(content_area); - self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout())); + if let Some(ignore_ms) = self.ignore_second_button_ms { + self.buttons = Child::new( + ButtonController::new(self.current_page.btn_layout()) + .with_ignore_btn_delay(ignore_ms), + ); + } else { + self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout())); + } self.pad.place(title_content_area); self.buttons.place(button_area); diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs index 1172976471..21d45d752a 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/choice.rs @@ -3,10 +3,13 @@ use crate::{ ui::{ component::{Child, Component, Event, EventCtx, Pad}, geometry::{Insets, Offset, Rect}, + util::animation_disabled, }, }; -use super::super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos}; +use super::super::{ + constant, theme, AutomaticMover, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos, +}; const DEFAULT_ITEMS_DISTANCE: i16 = 10; @@ -79,6 +82,13 @@ where /// Whether the middle selected item should be painted with /// inverse colors - black on white. inverse_selected_item: bool, + /// For moving through the items when holding left/right button + holding_mover: AutomaticMover, + /// For doing quick animations when changing the page counter. + animation_mover: AutomaticMover, + /// How many animated steps we should still do (positive for right, negative + /// for left). + animated_steps_to_do: i16, } impl ChoicePage @@ -89,16 +99,30 @@ where pub fn new(choices: F) -> Self { let initial_btn_layout = choices.get(0).0.btn_layout(); + /// First move happens immediately, then in 35 ms intervals + fn animation_duration_func(steps: usize) -> u32 { + match steps { + 0 => 0, + _ => 35, + } + } + Self { choices, pad: Pad::with_background(theme::BG), - buttons: Child::new(ButtonController::new(initial_btn_layout)), + buttons: Child::new( + ButtonController::new(initial_btn_layout) + .with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS), + ), page_counter: 0, items_distance: DEFAULT_ITEMS_DISTANCE, is_carousel: false, show_incomplete: false, show_only_one_item: false, inverse_selected_item: false, + holding_mover: AutomaticMover::new(), + animation_mover: AutomaticMover::new().with_duration_func(animation_duration_func), + animated_steps_to_do: 0, } } @@ -107,7 +131,10 @@ where pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self { self.page_counter = page_counter; let initial_btn_layout = self.get_current_choice().0.btn_layout(); - self.buttons = Child::new(ButtonController::new(initial_btn_layout)); + self.buttons = Child::new( + ButtonController::new(initial_btn_layout) + .with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS), + ); self } @@ -156,9 +183,33 @@ where } /// Navigating to the chosen page index. - pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: usize) { - self.page_counter = page_counter; - self.update(ctx); + pub fn set_page_counter( + &mut self, + ctx: &mut EventCtx, + page_counter: usize, + do_animation: bool, + ) { + // Either moving with animation or just jumping to the final position directly. + if do_animation && !animation_disabled() { + let diff = page_counter as i16 - self.page_counter as i16; + // When there would be a small number of animation frames (3 or less), + // animating in the opposite direction to make the animation longer. + self.animated_steps_to_do = match diff { + -3..=0 => diff + self.choices.count() as i16, + 1..=3 => diff - self.choices.count() as i16, + _ => diff, + }; + // Starting the movement immediately - either left or right. + let pos = if self.animated_steps_to_do > 0 { + ButtonPos::Right + } else { + ButtonPos::Left + }; + self.animation_mover.start_moving(ctx, pos); + } else { + self.page_counter = page_counter; + self.update(ctx); + } } /// Display current, previous and next choices according to @@ -356,14 +407,66 @@ where /// whether they are long-pressed, and painting them. fn set_buttons(&mut self, ctx: &mut EventCtx) { let btn_layout = self.get_current_choice().0.btn_layout(); - self.buttons.mutate(ctx, |_ctx, buttons| { + self.buttons.mutate(ctx, |ctx, buttons| { buttons.set(btn_layout); + // When user holds one of the buttons, highlighting it. + if let Some(btn_down) = self.holding_mover.moving_direction() { + buttons.highlight_button(ctx, btn_down); + } + ctx.request_paint(); }); } pub fn choice_factory(&self) -> &F { &self.choices } + + /// Go to the choice visually on the left. + fn move_left(&mut self, ctx: &mut EventCtx) { + if self.has_previous_choice() { + self.decrease_page_counter(); + self.update(ctx); + } else if self.is_carousel { + self.page_counter_to_max(); + self.update(ctx); + } + } + + /// Go to the choice visually on the right. + fn move_right(&mut self, ctx: &mut EventCtx) { + if self.has_next_choice() { + self.increase_page_counter(); + self.update(ctx); + } else if self.is_carousel { + self.page_counter_to_zero(); + self.update(ctx); + } + } + + /// Possibly doing an animation movement with the choice - either left or + /// right. + fn animation_event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + if animation_disabled() { + return None; + } + // Stopping the movement if it is moving and there are no steps left + if self.animated_steps_to_do == 0 { + if self.animation_mover.is_moving() { + self.animation_mover.stop_moving(); + } + return None; + } + let animation_result = self.animation_mover.event(ctx, event); + // When about to animate, decreasing the number of steps to do. + if animation_result.is_some() { + if self.animated_steps_to_do > 0 { + self.animated_steps_to_do -= 1; + } else { + self.animated_steps_to_do += 1; + } + } + animation_result + } } impl Component for ChoicePage @@ -381,32 +484,67 @@ where } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { + // Possible animation movement when setting (randomizing) the page counter. + if let Some(animation_direction) = self.animation_event(ctx, event) { + match animation_direction { + ButtonPos::Left => self.move_left(ctx), + ButtonPos::Right => self.move_right(ctx), + _ => {} + } + return None; + } + + // When animation is running, ignoring all user events + if self.animation_mover.is_moving() { + return None; + } + + // Possible automatic movement when user is holding left or right button. + if let Some(auto_move_direction) = self.holding_mover.event(ctx, event) { + match auto_move_direction { + ButtonPos::Left => self.move_left(ctx), + ButtonPos::Right => self.move_right(ctx), + _ => {} + } + return None; + } + let button_event = self.buttons.event(ctx, event); - // Button was "triggered" - released. Doing the appropriate action. + // Possibly stopping or starting the automatic mover. + if let Some(moving_direction) = self.holding_mover.moving_direction() { + // Stopping the automatic movement when the released button is the same as the + // direction we were moving, or when the pressed button is the + // opposite one (user does middle-click). + if matches!(button_event, Some(ButtonControllerMsg::Triggered(pos)) if pos == moving_direction) + || matches!(button_event, Some(ButtonControllerMsg::Pressed(pos)) if pos != moving_direction) + { + self.holding_mover.stop_moving(); + // Ignoring the event when it already did some automatic movements. (Otherwise + // it would do one more movement.) + if self.holding_mover.was_moving() { + return None; + } + } + } else if let Some(ButtonControllerMsg::Pressed(pos)) = button_event { + // Starting the movement when left/right button is pressed. + if matches!(pos, ButtonPos::Left | ButtonPos::Right) { + self.holding_mover.start_moving(ctx, pos); + } + } + + // There was a legitimate button event - doing some action if let Some(ButtonControllerMsg::Triggered(pos)) = button_event { match pos { ButtonPos::Left => { - if self.has_previous_choice() { - // Clicked BACK. Decrease the page counter. - self.decrease_page_counter(); - self.update(ctx); - } else if self.is_carousel { - // In case of carousel going to the right end. - self.page_counter_to_max(); - self.update(ctx); - } + // Clicked BACK. Decrease the page counter. + // In case of carousel going to the right end. + self.move_left(ctx); } ButtonPos::Right => { - if self.has_next_choice() { - // Clicked NEXT. Increase the page counter. - self.increase_page_counter(); - self.update(ctx); - } else if self.is_carousel { - // In case of carousel going to the left end. - self.page_counter_to_zero(); - self.update(ctx); - } + // Clicked NEXT. Increase the page counter. + // In case of carousel going to the left end. + self.move_right(ctx); } ButtonPos::Middle => { // Clicked SELECT. Send current choice index @@ -415,7 +553,7 @@ where } } }; - // The middle button was "pressed", highlighting the current choice by color + // The middle button was pressed, highlighting the current choice by color // inversion. if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event { self.inverse_selected_item = true; diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs index a408069b5c..c13e480366 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/passphrase.rs @@ -323,8 +323,11 @@ where /// Randomly choose an index in the current category fn randomize_category_position(&mut self, ctx: &mut EventCtx) { - self.choice_page - .set_page_counter(ctx, random_category_position(&self.current_category)); + self.choice_page.set_page_counter( + ctx, + random_category_position(&self.current_category), + true, + ); } } diff --git a/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs b/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs index e989078be9..ef3495631f 100644 --- a/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs +++ b/core/embed/rust/src/ui/model_tr/component/input_methods/pin.rs @@ -219,15 +219,17 @@ where } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - // Any event when showing real PIN should hide it + // Any non-timer event when showing real PIN should hide it // Same with showing last digit - if self.show_real_pin { - self.show_real_pin = false; - self.update(ctx) - } - if self.show_last_digit { - self.show_last_digit = false; - self.update(ctx) + if !matches!(event, Event::Timer(_)) { + if self.show_real_pin { + self.show_real_pin = false; + self.update(ctx) + } + if self.show_last_digit { + self.show_last_digit = false; + self.update(ctx) + } } // Any button event will show the "real" prompt @@ -254,7 +256,7 @@ where self.textbox.append(ctx, ch); // Choosing random digit to be shown next self.choice_page - .set_page_counter(ctx, get_random_digit_position()); + .set_page_counter(ctx, get_random_digit_position(), true); self.show_last_digit = true; self.update(ctx); None diff --git a/core/embed/rust/src/ui/model_tr/component/mod.rs b/core/embed/rust/src/ui/model_tr/component/mod.rs index f2baee2524..c116265770 100644 --- a/core/embed/rust/src/ui/model_tr/component/mod.rs +++ b/core/embed/rust/src/ui/model_tr/component/mod.rs @@ -13,7 +13,7 @@ pub use button::{ Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonPos, ButtonStyle, ButtonStyleSheet, }; -pub use button_controller::{ButtonController, ButtonControllerMsg}; +pub use button_controller::{AutomaticMover, ButtonController, ButtonControllerMsg}; pub use common_messages::CancelConfirmMsg; pub use error::ErrorScreen; pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg}; diff --git a/core/embed/rust/src/ui/model_tr/constant.rs b/core/embed/rust/src/ui/model_tr/constant.rs index e897f9d660..2e3c4434cd 100644 --- a/core/embed/rust/src/ui/model_tr/constant.rs +++ b/core/embed/rust/src/ui/model_tr/constant.rs @@ -22,3 +22,5 @@ pub const fn screen() -> Rect { Rect::from_top_left_and_size(Point::zero(), SIZE) } pub const SCREEN: Rect = screen(); + +pub const IGNORE_OTHER_BTN_MS: u32 = 200; diff --git a/core/embed/rust/src/ui/model_tr/layout.rs b/core/embed/rust/src/ui/model_tr/layout.rs index 2146897b56..6b80596fbf 100644 --- a/core/embed/rust/src/ui/model_tr/layout.rs +++ b/core/embed/rust/src/ui/model_tr/layout.rs @@ -859,7 +859,14 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj let pages = FlowPages::new(get_page, PAGE_COUNT); - let obj = LayoutObj::new(Flow::new(pages).with_scrollbar(false))?; + // Setting the ignore-second-button to mimic all the Choice pages, to teach user + // that they should really press both buttons at the same time to achieve + // middle-click. + let obj = LayoutObj::new( + Flow::new(pages) + .with_scrollbar(false) + .with_ignore_second_button_ms(constant::IGNORE_OTHER_BTN_MS), + )?; Ok(obj.into()) }; unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }