cursor: measure the idle-hide deadline from the last pointer event

refresh_idle_timer armed for the full timeout measured from now, which is
right only for notify_cursor_activity — it stamps activity immediately
before arming. The other callers deliberately do not stamp, so with
idle_timeout: Some(10) a user typing continuously never saw the cursor
hide: every key press and release pushed the deadline out by a fresh ten
seconds. That inverts the feature and regresses the shipped
cursor_hide_timeout behaviour.

resolve's RearmAfter cannot recover it — it only corrects a timer that
fires too early. So the delay is now shortest_timeout minus the time
already elapsed, extracted as CursorHideConfig::arm_delay and table
tested; the arithmetic handed to Timer::from_duration was the one part of
this path with no coverage. A zero delay is fine: the timer fires on the
next loop iteration and resolve decides.

Also rename fullscreen_focused to fullscreen_on_active_output, which is
what seat.active_output() actually reports, and log an arming failure
rather than silently disabling cursor hiding.

Drafted with Claude (Anthropic); reviewed and tested by the committer.
This commit is contained in:
Niklas Herder 2026-09-12 09:48:09 +02:00 • committed by Victoria Brekenfeld
parent 7596cf7f2f
commit 115cdac039
2 changed files with 48 additions and 20 deletions

View file

@ -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<Duration> {
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<Duration> {
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);
}
}