diff --git a/core/.changelog.d/3633.changed b/core/.changelog.d/3633.changed new file mode 100644 index 00000000000..dcc32f9a268 --- /dev/null +++ b/core/.changelog.d/3633.changed @@ -0,0 +1 @@ +Improved device responsiveness by removing unnecessary screen refreshes. diff --git a/core/embed/rust/src/ui/component/base.rs b/core/embed/rust/src/ui/component/base.rs index c07fc8dada4..a41a1064156 100644 --- a/core/embed/rust/src/ui/component/base.rs +++ b/core/embed/rust/src/ui/component/base.rs @@ -452,6 +452,42 @@ impl TimerToken { } } +#[cfg_attr(feature = "debug", derive(ufmt::derive::uDebug))] +pub struct Timer(Option); + +impl Timer { + /// Create a new timer. + pub const fn new() -> Self { + Self(None) + } + + /// Start this timer for a given duration. + /// + /// Requests the internal timer token to be scheduled to `duration` from + /// now. If the timer was already running, its token is rescheduled. + pub fn start(&mut self, ctx: &mut EventCtx, duration: Duration) { + let token = self.0.get_or_insert_with(|| ctx.next_timer_token()); + ctx.register_timer(*token, duration); + } + + /// Stop the timer. + /// + /// Does not affect scheduling, only clears the internal timer token. This + /// means that _some_ scheduled task might keep running, but this timer + /// will not trigger when that task expires. + pub fn stop(&mut self) { + self.0 = None; + } + + /// Check if the timer has expired. + /// + /// Returns `true` if the given event is a timer event and the token matches + /// the internal token of this timer. + pub fn is_expired(&self, event: Event) -> bool { + matches!(event, Event::Timer(token) if self.0 == Some(token)) + } +} + pub struct EventCtx { timers: Vec<(TimerToken, Duration), { Self::MAX_TIMERS }>, next_token: u32, @@ -508,13 +544,6 @@ impl EventCtx { self.paint_requested = true; } - /// Request a timer event to be delivered after `duration` elapses. - pub fn request_timer(&mut self, duration: Duration) -> TimerToken { - let token = self.next_timer_token(); - self.register_timer(token, duration); - token - } - /// Request an animation frame timer to fire as soon as possible. pub fn request_anim_frame(&mut self) { if !self.anim_frame_scheduled { @@ -523,6 +552,10 @@ impl EventCtx { } } + pub fn is_anim_frame(event: Event) -> bool { + matches!(event, Event::Timer(token) if token == Self::ANIM_FRAME_TIMER) + } + pub fn request_repaint_root(&mut self) { self.root_repaint_requested = true; } diff --git a/core/embed/rust/src/ui/component/marquee.rs b/core/embed/rust/src/ui/component/marquee.rs index c2f39a0819e..d1d932358d1 100644 --- a/core/embed/rust/src/ui/component/marquee.rs +++ b/core/embed/rust/src/ui/component/marquee.rs @@ -3,7 +3,7 @@ use crate::{ time::{Duration, Instant}, ui::{ animation::Animation, - component::{Component, Event, EventCtx, Never, TimerToken}, + component::{Component, Event, EventCtx, Never, Timer}, display, display::{Color, Font}, geometry::Rect, @@ -24,7 +24,7 @@ enum State { pub struct Marquee { area: Rect, - pause_token: Option, + pause_timer: Timer, min_offset: i16, max_offset: i16, state: State, @@ -40,7 +40,7 @@ impl Marquee { pub fn new(text: TString<'static>, font: Font, fg: Color, bg: Color) -> Self { Self { area: Rect::zero(), - pause_token: None, + pause_timer: Timer::new(), min_offset: 0, max_offset: 0, state: State::Initial, @@ -141,53 +141,50 @@ impl Component for Marquee { let now = Instant::now(); - if let Event::Timer(token) = event { - if self.pause_token == Some(token) { - match self.state { - State::PauseLeft => { - let anim = - Animation::new(self.max_offset, self.min_offset, self.duration, now); - self.state = State::Right(anim); - } - State::PauseRight => { - let anim = - Animation::new(self.min_offset, self.max_offset, self.duration, now); - self.state = State::Left(anim); - } - _ => {} + if self.pause_timer.is_expired(event) { + match self.state { + State::PauseLeft => { + let anim = Animation::new(self.max_offset, self.min_offset, self.duration, now); + self.state = State::Right(anim); + } + State::PauseRight => { + let anim = Animation::new(self.min_offset, self.max_offset, self.duration, now); + self.state = State::Left(anim); } + _ => {} + } + // We have something to paint, so request to be painted in the next pass. + ctx.request_paint(); + // There is further progress in the animation, request an animation frame event. + ctx.request_anim_frame(); + } + + if EventCtx::is_anim_frame(event) { + if self.is_animating() { // We have something to paint, so request to be painted in the next pass. ctx.request_paint(); - // There is further progress in the animation, request an animation frame event. + // There is further progress in the animation, request an animation frame + // event. ctx.request_anim_frame(); } - if token == EventCtx::ANIM_FRAME_TIMER { - if self.is_animating() { - // We have something to paint, so request to be painted in the next pass. - ctx.request_paint(); - // There is further progress in the animation, request an animation frame - // event. - ctx.request_anim_frame(); - } - - match self.state { - State::Right(_) => { - if self.is_at_right(now) { - self.pause_token = Some(ctx.request_timer(self.pause)); - self.state = State::PauseRight; - } + match self.state { + State::Right(_) => { + if self.is_at_right(now) { + self.pause_timer.start(ctx, self.pause); + self.state = State::PauseRight; } - State::Left(_) => { - if self.is_at_left(now) { - self.pause_token = Some(ctx.request_timer(self.pause)); - self.state = State::PauseLeft; - } + } + State::Left(_) => { + if self.is_at_left(now) { + self.pause_timer.start(ctx, self.pause); + self.state = State::PauseLeft; } - _ => {} } + _ => {} } } + None } diff --git a/core/embed/rust/src/ui/component/mod.rs b/core/embed/rust/src/ui/component/mod.rs index bc54734d9cd..e76acd28246 100644 --- a/core/embed/rust/src/ui/component/mod.rs +++ b/core/embed/rust/src/ui/component/mod.rs @@ -17,7 +17,7 @@ pub mod qr_code; pub mod text; pub mod timeout; -pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, Root, TimerToken}; +pub use base::{Child, Component, ComponentExt, Event, EventCtx, Never, Root, Timer}; pub use border::Border; pub use empty::Empty; pub use label::Label; diff --git a/core/embed/rust/src/ui/component/timeout.rs b/core/embed/rust/src/ui/component/timeout.rs index 67d29c75041..07118b8caeb 100644 --- a/core/embed/rust/src/ui/component/timeout.rs +++ b/core/embed/rust/src/ui/component/timeout.rs @@ -1,21 +1,21 @@ use crate::{ time::Duration, ui::{ - component::{Component, Event, EventCtx, TimerToken}, + component::{Component, Event, EventCtx, Timer}, geometry::Rect, }, }; pub struct Timeout { time_ms: u32, - timer: Option, + timer: Timer, } impl Timeout { pub fn new(time_ms: u32) -> Self { Self { time_ms, - timer: None, + timer: Timer::new(), } } } @@ -28,19 +28,10 @@ impl Component for Timeout { } fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option { - match event { - // Set up timer. - Event::Attach => { - self.timer = Some(ctx.request_timer(Duration::from_millis(self.time_ms))); - None - } - // Fire. - Event::Timer(token) if Some(token) == self.timer => { - self.timer = None; - Some(()) - } - _ => None, + if matches!(event, Event::Attach) { + self.timer.start(ctx, Duration::from_millis(self.time_ms)); } + self.timer.is_expired(event).then_some(()) } fn paint(&mut self) {} diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs index 03d7303845c..a83200bbcb8 100644 --- a/core/embed/rust/src/ui/layout/obj.rs +++ b/core/embed/rust/src/ui/layout/obj.rs @@ -17,7 +17,7 @@ use crate::{ }, time::Duration, ui::{ - component::{Component, Event, EventCtx, Never, Root, TimerToken}, + component::{base::TimerToken, Component, Event, EventCtx, Never, Root}, constant, display::sync, geometry::Rect, 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 76d053faa96..2b1b0226255 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 @@ -4,7 +4,7 @@ use super::{ use crate::{ time::{Duration, Instant}, ui::{ - component::{base::Event, Component, EventCtx, Pad, TimerToken}, + component::{base::Event, Component, EventCtx, Pad, Timer}, event::{ButtonEvent, PhysicalButton}, geometry::Rect, }, @@ -109,7 +109,7 @@ pub struct ButtonContainer { /// `ButtonControllerMsg::Triggered` long_press_ms: u32, /// Timer for sending `ButtonControllerMsg::LongPressed` - long_pressed_timer: Option, + long_pressed_timer: Timer, /// Whether it should even send `ButtonControllerMsg::LongPressed` events /// (optional) send_long_press: bool, @@ -128,7 +128,7 @@ impl ButtonContainer { button_type: ButtonType::from_button_details(pos, btn_details), pressed_since: None, long_press_ms: DEFAULT_LONG_PRESS_MS, - long_pressed_timer: None, + long_pressed_timer: Timer::new(), send_long_press, } } @@ -173,7 +173,7 @@ impl ButtonContainer { Instant::now().saturating_duration_since(since).to_millis() > self.long_press_ms }); self.pressed_since = None; - self.long_pressed_timer = None; + self.long_pressed_timer.stop(); Some(ButtonControllerMsg::Triggered(self.pos, long_press)) } ButtonType::HoldToConfirm(_) => { @@ -199,20 +199,20 @@ impl ButtonContainer { pub fn got_pressed(&mut self, ctx: &mut EventCtx) { self.pressed_since = Some(Instant::now()); if self.send_long_press { - self.long_pressed_timer = - Some(ctx.request_timer(Duration::from_millis(self.long_press_ms))); + self.long_pressed_timer + .start(ctx, Duration::from_millis(self.long_press_ms)); } } /// Reset the pressed information. pub fn reset(&mut self) { self.pressed_since = None; - self.long_pressed_timer = None; + self.long_pressed_timer.stop(); } /// Whether token matches what we have - pub fn is_timer_token(&self, token: TimerToken) -> bool { - self.long_pressed_timer == Some(token) + pub fn is_timer(&self, event: Event) -> bool { + self.long_pressed_timer.is_expired(event) } /// Registering hold event. @@ -363,14 +363,14 @@ impl ButtonController { } } - fn handle_long_press_timer_token(&mut self, token: TimerToken) -> Option { - if self.left_btn.is_timer_token(token) { + fn handle_long_press_timers(&mut self, event: Event) -> Option { + if self.left_btn.is_timer(event) { return Some(ButtonPos::Left); } - if self.middle_btn.is_timer_token(token) { + if self.middle_btn.is_timer(event) { return Some(ButtonPos::Middle); } - if self.right_btn.is_timer_token(token) { + if self.right_btn.is_timer(event) { return Some(ButtonPos::Right); } None @@ -555,11 +555,11 @@ impl Component for ButtonController { event } // Timer - handle clickable properties and HoldToConfirm expiration - Event::Timer(token) => { + Event::Timer(_) => { if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay { - ignore_btn_delay.handle_timer_token(token); + ignore_btn_delay.handle_timers(event); } - if let Some(pos) = self.handle_long_press_timer_token(token) { + if let Some(pos) = self.handle_long_press_timers(event) { return Some(ButtonControllerMsg::LongPressed(pos)); } self.handle_htc_expiration(ctx, event) @@ -600,9 +600,9 @@ struct IgnoreButtonDelay { /// Whether right button is currently clickable right_clickable: bool, /// Timer for setting the left_clickable - left_clickable_timer: Option, + left_clickable_timer: Timer, /// Timer for setting the right_clickable - right_clickable_timer: Option, + right_clickable_timer: Timer, } impl IgnoreButtonDelay { @@ -611,8 +611,8 @@ impl IgnoreButtonDelay { delay: Duration::from_millis(delay_ms), left_clickable: true, right_clickable: true, - left_clickable_timer: None, - right_clickable_timer: None, + left_clickable_timer: Timer::new(), + right_clickable_timer: Timer::new(), } } @@ -620,11 +620,11 @@ impl IgnoreButtonDelay { match pos { ButtonPos::Left => { self.left_clickable = true; - self.left_clickable_timer = None; + self.left_clickable_timer.stop(); } ButtonPos::Right => { self.right_clickable = true; - self.right_clickable_timer = None; + self.right_clickable_timer.stop(); } ButtonPos::Middle => {} } @@ -632,10 +632,10 @@ impl IgnoreButtonDelay { 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)); + self.right_clickable_timer.start(ctx, self.delay); } if matches!(button, PhysicalButton::Right) { - self.left_clickable_timer = Some(ctx.request_timer(self.delay)); + self.left_clickable_timer.start(ctx, self.delay); } } @@ -649,22 +649,20 @@ impl IgnoreButtonDelay { false } - pub fn handle_timer_token(&mut self, token: TimerToken) { - if self.left_clickable_timer == Some(token) { + pub fn handle_timers(&mut self, event: Event) { + if self.left_clickable_timer.is_expired(event) { self.left_clickable = false; - self.left_clickable_timer = None; } - if self.right_clickable_timer == Some(token) { + if self.right_clickable_timer.is_expired(event) { self.right_clickable = false; - self.right_clickable_timer = None; } } pub fn reset(&mut self) { self.left_clickable = true; self.right_clickable = true; - self.left_clickable_timer = None; - self.right_clickable_timer = None; + self.left_clickable_timer.stop(); + self.right_clickable_timer.stop(); } } @@ -676,7 +674,7 @@ impl IgnoreButtonDelay { /// Can be started e.g. by holding left/right button. pub struct AutomaticMover { /// For requesting timer events repeatedly - timer_token: Option, + timer: Timer, /// Which direction should we go (which button is down) moving_direction: Option, /// How many screens were moved automatically @@ -697,7 +695,7 @@ impl AutomaticMover { } Self { - timer_token: None, + timer: Timer::new(), moving_direction: None, auto_moved_screens: 0, duration_func: default_duration_func, @@ -736,12 +734,12 @@ impl AutomaticMover { 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())); + self.timer.start(ctx, self.get_auto_move_duration()); } pub fn stop_moving(&mut self) { self.moving_direction = None; - self.timer_token = None; + self.timer.stop(); } } @@ -755,15 +753,11 @@ impl Component for AutomaticMover { 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; - } + if self.timer.is_expired(event) && self.moving_direction.is_some() { + // Restart timer and send the appropriate button trigger event + self.timer.start(ctx, self.get_auto_move_duration()); + self.auto_moved_screens += 1; + return self.moving_direction; } None } diff --git a/core/embed/rust/src/ui/model_tt/component/button.rs b/core/embed/rust/src/ui/model_tt/component/button.rs index fdf11ca15f7..ea78714845f 100644 --- a/core/embed/rust/src/ui/model_tt/component/button.rs +++ b/core/embed/rust/src/ui/model_tt/component/button.rs @@ -5,7 +5,7 @@ use crate::{ time::Duration, ui::{ component::{ - Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, TimerToken, + Component, ComponentExt, Event, EventCtx, FixedHeightBar, MsgMap, Split, Timer, }, display::{self, toif::Icon, Color, Font}, event::TouchEvent, @@ -29,7 +29,7 @@ pub struct Button { styles: ButtonStyleSheet, state: State, long_press: Option, - long_timer: Option, + long_timer: Timer, } impl Button { @@ -45,7 +45,7 @@ impl Button { styles: theme::button_default(), state: State::Initial, long_press: None, - long_timer: None, + long_timer: Timer::new(), } } @@ -255,7 +255,7 @@ impl Component for Button { play(HapticEffect::ButtonPress); self.set(ctx, State::Pressed); if let Some(duration) = self.long_press { - self.long_timer = Some(ctx.request_timer(duration)); + self.long_timer.start(ctx, duration) } return Some(ButtonMsg::Pressed); } @@ -287,19 +287,16 @@ impl Component for Button { _ => { // Touch finished outside our area. self.set(ctx, State::Initial); - self.long_timer = None; + self.long_timer.stop(); } } } - Event::Timer(token) => { - if self.long_timer == Some(token) { - self.long_timer = None; - if matches!(self.state, State::Pressed) { - #[cfg(feature = "haptic")] - play(HapticEffect::ButtonPress); - self.set(ctx, State::Initial); - return Some(ButtonMsg::LongPressed); - } + Event::Timer(_) if self.long_timer.is_expired(event) => { + if matches!(self.state, State::Pressed) { + #[cfg(feature = "haptic")] + play(HapticEffect::ButtonPress); + self.set(ctx, State::Initial); + return Some(ButtonMsg::LongPressed); } } _ => {} diff --git a/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs b/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs index 1033139f5de..2b2d7cfb020 100644 --- a/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs +++ b/core/embed/rust/src/ui/model_tt/component/homescreen/mod.rs @@ -6,7 +6,7 @@ use crate::{ translations::TR, trezorhal::usb::usb_configured, ui::{ - component::{Component, Event, EventCtx, Pad, TimerToken}, + component::{Component, Event, EventCtx, Pad, Timer}, display::{self, tjpgd::jpeg_info, toif::Icon, Color, Font}, event::{TouchEvent, USBEvent}, geometry::{Offset, Point, Rect}, @@ -53,7 +53,7 @@ pub struct Homescreen { loader: Loader, pad: Pad, paint_notification_only: bool, - delay: Option, + delay: Timer, } pub enum HomescreenMsg { @@ -73,7 +73,7 @@ impl Homescreen { loader: Loader::with_lock_icon().with_durations(LOADER_DURATION, LOADER_DURATION / 3), pad: Pad::with_background(theme::BG), paint_notification_only: false, - delay: None, + delay: Timer::new(), } } @@ -136,11 +136,11 @@ impl Homescreen { if self.loader.is_animating() { self.loader.start_growing(ctx, Instant::now()); } else { - self.delay = Some(ctx.request_timer(LOADER_DELAY)); + self.delay.start(ctx, LOADER_DELAY); } } Event::Touch(TouchEvent::TouchEnd(_)) => { - self.delay = None; + self.delay.stop(); let now = Instant::now(); if self.loader.is_completely_grown(now) { return true; @@ -149,8 +149,7 @@ impl Homescreen { self.loader.start_shrinking(ctx, now); } } - Event::Timer(token) if Some(token) == self.delay => { - self.delay = None; + Event::Timer(_) if self.delay.is_expired(event) => { self.pad.clear(); self.paint_notification_only = false; self.loader.start_growing(ctx, Instant::now()); diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs index 9137ca189bc..c2607ddaf67 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/common.rs @@ -1,7 +1,7 @@ use crate::{ time::Duration, ui::{ - component::{text::common::TextEdit, Event, EventCtx, TimerToken}, + component::{text::common::TextEdit, Event, EventCtx, Timer}, display::{self, Color, Font}, geometry::{Offset, Point, Rect}, }, @@ -22,7 +22,16 @@ struct Pending { /// one). press: usize, /// Timer for clearing the pending state. - timer: TimerToken, + timer: Timer, +} + +impl Pending { + /// Create a new pending state for a key. + fn start(ctx: &mut EventCtx, key: usize, press: usize, timeout: Duration) -> Self { + let mut timer = Timer::new(); + timer.start(ctx, timeout); + Self { key, press, timer } + } } impl MultiTapKeyboard { @@ -45,14 +54,14 @@ impl MultiTapKeyboard { } /// Return the token for the currently pending timer. - pub fn pending_timer(&self) -> Option { - self.pending.as_ref().map(|p| p.timer) + pub fn pending_timer(&self) -> Option<&Timer> { + self.pending.as_ref().map(|p| &p.timer) } /// Returns `true` if `event` is an `Event::Timer` for the currently pending /// timer. pub fn is_timeout_event(&self, event: Event) -> bool { - matches!((event, self.pending_timer()), (Event::Timer(t), Some(pt)) if pt == t) + self.pending_timer().map_or(false, |t| t.is_expired(event)) } /// Reset to the empty state. Takes `EventCtx` to request a paint pass (to @@ -93,11 +102,7 @@ impl MultiTapKeyboard { // transition only happens as a result of an append op, so the painting should // be requested by handling the `TextEdit`. self.pending = if key_text.len() > 1 { - Some(Pending { - key, - press, - timer: ctx.request_timer(self.timeout), - }) + Some(Pending::start(ctx, key, press, self.timeout)) } else { None }; diff --git a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs index 7c5a74c73ab..31818d4108b 100644 --- a/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs +++ b/core/embed/rust/src/ui/model_tt/component/keyboard/pin.rs @@ -8,7 +8,7 @@ use crate::{ ui::{ component::{ base::ComponentExt, text::TextStyle, Child, Component, Event, EventCtx, Label, Maybe, - Never, Pad, TimerToken, + Never, Pad, Timer, }, display::{self, Font}, event::TouchEvent, @@ -51,7 +51,7 @@ pub struct PinKeyboard<'a> { cancel_btn: Child>, confirm_btn: Child