diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs index 41b4dd2d..193fd0db 100644 --- a/cosmic-comp-config/src/lib.rs +++ b/cosmic-comp-config/src/lib.rs @@ -251,11 +251,15 @@ impl CursorHideConfig { } } - /// The delay to arm the timer with, before context is known. Callers on the - /// input path cannot read fullscreen state without deadlocking, so they arm - /// pessimistically and `resolve` corrects it on fire. - pub fn shortest_timeout(&self) -> Option { - self.effective_timeout(true) + /// How long to wait before the idle timer should next fire, given how long + /// it has been since the last pointer event. Pessimistic: fullscreen only + /// ever shortens a timeout, so arming as if fullscreen cannot fire too + /// late, and `resolve` corrects an early fire once the context is known. + pub fn arm_delay(&self, since_pointer_activity: Duration) -> Option { + Some( + self.effective_timeout(true)? + .saturating_sub(since_pointer_activity), + ) } } @@ -402,12 +406,6 @@ mod test { ..OFF }; assert_eq!(long_fs.resolve(secs(3), true), HideDecision::Hide); - - // The arming delay is pessimistic: the shortest timeout that could - // apply in any context, because arm-time code cannot read the shell. - assert_eq!(short_fs.shortest_timeout(), Some(secs(3))); - assert_eq!(long_fs.shortest_timeout(), Some(secs(3))); - assert_eq!(OFF.shortest_timeout(), None); } #[test] @@ -423,4 +421,27 @@ mod test { assert_eq!(cfg.resolve(secs(5), false), HideDecision::Hide); assert_eq!(cfg.resolve(secs(6), false), HideDecision::Hide); } + + #[test] + fn arm_delay_measures_from_the_last_pointer_event() { + let cfg = CursorHideConfig { + idle_timeout: Some(10), + ..OFF + }; + assert_eq!(cfg.arm_delay(Duration::ZERO), Some(secs(10))); + assert_eq!(cfg.arm_delay(secs(4)), Some(secs(6))); + // Exactly due, and past due: fire immediately rather than deferring. + assert_eq!(cfg.arm_delay(secs(10)), Some(Duration::ZERO)); + assert_eq!(cfg.arm_delay(secs(600)), Some(Duration::ZERO)); + + // Armed against the shortest timeout that any context could want. + let fs = CursorHideConfig { + idle_timeout: Some(10), + fullscreen_idle_timeout: Some(3), + ..OFF + }; + assert_eq!(fs.arm_delay(secs(1)), Some(secs(2))); + + assert_eq!(OFF.arm_delay(Duration::ZERO), None); + } } diff --git a/src/backend/render/cursor.rs b/src/backend/render/cursor.rs index 24ad5a60..95fb7500 100644 --- a/src/backend/render/cursor.rs +++ b/src/backend/render/cursor.rs @@ -892,6 +892,10 @@ pub fn notify_cursor_activity(state: &State, seat: &Seat, kind: PointerEv /// (Re)arm the idle-hide timer without counting as pointer activity. /// +/// The deadline is measured from the last *pointer* event, not from now, so a +/// caller that does not stamp activity — a keystroke, a config reload — cannot +/// postpone the hide by re-arming. +/// /// Armed with the shortest timeout any context could want: the input that arms /// the timer is usually what goes on to make a window fullscreen, so the /// context is not yet knowable here. `on_idle_timer` evaluates it on fire and @@ -902,7 +906,7 @@ pub fn refresh_idle_timer(state: &State, seat: &Seat) { let cursor_state = seat.user_data().get::().unwrap(); let now = Instant::now(); - let old_token = { + let (old_token, since_activity) = { let mut inner = cursor_state.lock().unwrap(); if inner.hidden.is_some() { return; @@ -915,26 +919,29 @@ pub fn refresh_idle_timer(state: &State, seat: &Seat) { if throttled { return; } + let since_activity = now.duration_since(inner.last_pointer_activity); inner.last_armed = None; - inner.idle_timer.take() + (inner.idle_timer.take(), since_activity) }; if let Some(token) = old_token { loop_handle.remove(token); } - let Some(delay) = config.shortest_timeout() else { + let Some(delay) = config.arm_delay(since_activity) else { return; }; let timer = Timer::from_duration(delay); let timer_seat = seat.clone(); - if let Ok(token) = - loop_handle.insert_source(timer, move |_, _, state| on_idle_timer(state, &timer_seat)) - { - let mut inner = cursor_state.lock().unwrap(); - inner.idle_timer = Some(token); - inner.last_armed = Some(now); + match loop_handle.insert_source(timer, move |_, _, state| on_idle_timer(state, &timer_seat)) { + Ok(token) => { + let mut inner = cursor_state.lock().unwrap(); + inner.idle_timer = Some(token); + inner.last_armed = Some(now); + } + // `idle_timer` stays `None`, so the next activity is unthrottled and retries. + Err(err) => warn!(?err, "Failed to arm the cursor idle-hide timer"), } }