cursor: re-evaluate the idle timer in context, add fullscreen hiding
The timer now decides on fire rather than at arm time, so a shorter fullscreen timeout applies without hooking any of the six fullscreen transition sites in shell/mod.rs. Arming still touches no shell lock, which the PointerMotion call site requires. hidden becomes Option<HideReason> so the reveal rule can depend on why the cursor went away. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Drafted with Claude (Anthropic); reviewed and tested by the committer.
This commit is contained in:
parent
4e72d2ce98
commit
569cfa6442
3 changed files with 179 additions and 43 deletions
|
|
@ -5,9 +5,11 @@ use crate::{
|
|||
element::AsGlowRenderer,
|
||||
wayland::{SurfaceRenderElement, push_render_elements_from_surface_tree},
|
||||
},
|
||||
shell::focus::target::KeyboardFocusTarget,
|
||||
utils::prelude::*,
|
||||
wayland::handlers::compositor::FRAME_TIME_FILTER,
|
||||
};
|
||||
use cosmic_comp_config::HideDecision;
|
||||
use keyframe::{ease, functions::EaseInOutCubic};
|
||||
use resvg::{tiny_skia, usvg};
|
||||
use serde::Deserialize;
|
||||
|
|
@ -451,6 +453,32 @@ pub fn draw_dnd_icon<R>(
|
|||
);
|
||||
}
|
||||
|
||||
/// Why the cursor is currently hidden. Determines what brings it back.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HideReason {
|
||||
/// No pointer activity for the configured timeout.
|
||||
Idle,
|
||||
/// A key was pressed while `while_typing` is enabled.
|
||||
Typing,
|
||||
/// Touch input arrived while `after_touch` is enabled.
|
||||
Touch,
|
||||
}
|
||||
|
||||
/// Whether a pointer event carries movement. A touch-hidden cursor comes back
|
||||
/// only on real movement, so the distinction has to reach the call site.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PointerEventKind {
|
||||
Motion,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl HideReason {
|
||||
/// Whether an event of this kind should reveal the cursor.
|
||||
fn revealed_by(self, kind: PointerEventKind) -> bool {
|
||||
matches!(kind, PointerEventKind::Motion) || !matches!(self, HideReason::Touch)
|
||||
}
|
||||
}
|
||||
|
||||
pub type CursorState = Mutex<CursorStateInner>;
|
||||
pub struct CursorStateInner {
|
||||
current_cursor: Option<CursorIcon>,
|
||||
|
|
@ -462,9 +490,10 @@ pub struct CursorStateInner {
|
|||
current_image: Option<Image>,
|
||||
image_cache: Vec<CachedFrame>,
|
||||
|
||||
hidden: bool,
|
||||
hidden: Option<HideReason>,
|
||||
idle_timer: Option<RegistrationToken>,
|
||||
last_armed: Option<Instant>,
|
||||
last_pointer_activity: Instant,
|
||||
|
||||
// shake-to-find
|
||||
shake_path: VecDeque<PathSample>,
|
||||
|
|
@ -694,7 +723,8 @@ impl Default for CursorStateInner {
|
|||
current_image: None,
|
||||
image_cache: Vec::new(),
|
||||
|
||||
hidden: false,
|
||||
hidden: None,
|
||||
last_pointer_activity: Instant::now(),
|
||||
idle_timer: None,
|
||||
last_armed: None,
|
||||
|
||||
|
|
@ -747,7 +777,7 @@ pub fn draw_cursor<R>(
|
|||
let mut state_ref = seat_userdata.get::<CursorState>().unwrap().lock().unwrap();
|
||||
let state = &mut *state_ref;
|
||||
|
||||
if state.hidden {
|
||||
if state.hidden.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -837,54 +867,130 @@ pub fn draw_cursor<R>(
|
|||
|
||||
const ACTIVITY_THROTTLE: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Reveal the cursor and (re)arm the idle-hide timer; returns true if it was previously hidden
|
||||
pub fn notify_cursor_activity(state: &State, seat: &Seat<State>) -> bool {
|
||||
let timeout = state.common.config.cosmic_conf.cursor_hide.idle_timeout;
|
||||
/// Reveal the cursor if this event should, and (re)arm the idle-hide timer.
|
||||
/// Returns true if the cursor was previously hidden and is now visible.
|
||||
pub fn notify_cursor_activity(state: &State, seat: &Seat<State>, kind: PointerEventKind) -> bool {
|
||||
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||
|
||||
let revealed = {
|
||||
let mut inner = cursor_state.lock().unwrap();
|
||||
inner.last_pointer_activity = Instant::now();
|
||||
match inner.hidden {
|
||||
Some(reason) if reason.revealed_by(kind) => {
|
||||
inner.hidden = None;
|
||||
true
|
||||
}
|
||||
// Still hidden on purpose: leave the timer alone.
|
||||
Some(_) => return false,
|
||||
None => false,
|
||||
}
|
||||
};
|
||||
|
||||
refresh_idle_timer(state, seat);
|
||||
revealed
|
||||
}
|
||||
|
||||
/// (Re)arm the idle-hide timer without counting as pointer activity.
|
||||
///
|
||||
/// 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
|
||||
/// re-arms for the remainder if nothing applied yet.
|
||||
pub fn refresh_idle_timer(state: &State, seat: &Seat<State>) {
|
||||
let config = state.common.config.cosmic_conf.cursor_hide;
|
||||
let loop_handle = &state.common.event_loop_handle;
|
||||
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||
let now = Instant::now();
|
||||
|
||||
let (was_hidden, old_token) = {
|
||||
let old_token = {
|
||||
let mut inner = cursor_state.lock().unwrap();
|
||||
let was_hidden = inner.hidden;
|
||||
inner.hidden = false;
|
||||
|
||||
let throttled = timeout.is_some()
|
||||
&& !was_hidden
|
||||
&& inner.idle_timer.is_some()
|
||||
if inner.hidden.is_some() {
|
||||
return;
|
||||
}
|
||||
// Coalesce bursts from high-frequency pointers.
|
||||
let throttled = inner.idle_timer.is_some()
|
||||
&& inner
|
||||
.last_armed
|
||||
.is_some_and(|t| now.duration_since(t) < ACTIVITY_THROTTLE);
|
||||
if throttled {
|
||||
return was_hidden;
|
||||
return;
|
||||
}
|
||||
|
||||
let old_token = inner.idle_timer.take();
|
||||
inner.last_armed = None;
|
||||
(was_hidden, old_token)
|
||||
inner.idle_timer.take()
|
||||
};
|
||||
|
||||
if let Some(token) = old_token {
|
||||
loop_handle.remove(token);
|
||||
}
|
||||
|
||||
if let Some(secs) = timeout {
|
||||
let timer = Timer::from_duration(Duration::from_secs(secs as u64));
|
||||
let seat = seat.clone();
|
||||
if let Ok(token) = loop_handle.insert_source(timer, move |_, _, state| {
|
||||
hide_cursor(state, &seat);
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
let mut inner = cursor_state.lock().unwrap();
|
||||
inner.idle_timer = Some(token);
|
||||
inner.last_armed = Some(now);
|
||||
}
|
||||
}
|
||||
let Some(delay) = config.shortest_timeout() else {
|
||||
return;
|
||||
};
|
||||
|
||||
was_hidden
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_cursor(state: &mut State, seat: &Seat<State>) {
|
||||
/// Whether the seat is focusing a fullscreen surface. A fullscreen window that
|
||||
/// is not focused can be stacked behind others, so its presence on the output
|
||||
/// says nothing about what the user is looking at.
|
||||
fn fullscreen_focused(seat: &Seat<State>) -> bool {
|
||||
matches!(
|
||||
seat.get_keyboard().and_then(|kbd| kbd.current_focus()),
|
||||
Some(KeyboardFocusTarget::Fullscreen(_))
|
||||
)
|
||||
}
|
||||
|
||||
fn on_idle_timer(state: &mut State, seat: &Seat<State>) -> TimeoutAction {
|
||||
let config = state.common.config.cosmic_conf.cursor_hide;
|
||||
let fullscreen = fullscreen_focused(seat);
|
||||
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||
|
||||
let elapsed = {
|
||||
let inner = cursor_state.lock().unwrap();
|
||||
Instant::now().duration_since(inner.last_pointer_activity)
|
||||
};
|
||||
|
||||
match config.resolve(elapsed, fullscreen) {
|
||||
HideDecision::Hide => {
|
||||
hide_cursor(state, seat, HideReason::Idle);
|
||||
TimeoutAction::Drop
|
||||
}
|
||||
HideDecision::Drop => {
|
||||
let mut inner = cursor_state.lock().unwrap();
|
||||
inner.idle_timer = None;
|
||||
inner.last_armed = None;
|
||||
TimeoutAction::Drop
|
||||
}
|
||||
HideDecision::RearmAfter(delay) => {
|
||||
cursor_state.lock().unwrap().last_armed = Some(Instant::now());
|
||||
TimeoutAction::ToDuration(delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide the cursor immediately, for a trigger that does not wait on the timer.
|
||||
pub fn hide_cursor_now(state: &mut State, seat: &Seat<State>, reason: HideReason) {
|
||||
let loop_handle = state.common.event_loop_handle.clone();
|
||||
let token = {
|
||||
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||
let mut inner = cursor_state.lock().unwrap();
|
||||
inner.idle_timer.take()
|
||||
};
|
||||
if let Some(token) = token {
|
||||
loop_handle.remove(token);
|
||||
}
|
||||
hide_cursor(state, seat, reason);
|
||||
}
|
||||
|
||||
fn hide_cursor(state: &mut State, seat: &Seat<State>, reason: HideReason) {
|
||||
if let Some(ptr) = seat.get_pointer()
|
||||
&& ptr.is_grabbed()
|
||||
{
|
||||
|
|
@ -893,7 +999,10 @@ fn hide_cursor(state: &mut State, seat: &Seat<State>) {
|
|||
let cursor_state = seat.user_data().get::<CursorState>().unwrap();
|
||||
{
|
||||
let mut inner = cursor_state.lock().unwrap();
|
||||
inner.hidden = true;
|
||||
if inner.hidden.is_some() {
|
||||
return;
|
||||
}
|
||||
inner.hidden = Some(reason);
|
||||
inner.idle_timer = None;
|
||||
inner.last_armed = None;
|
||||
}
|
||||
|
|
@ -902,3 +1011,24 @@ fn hide_cursor(state: &mut State, seat: &Seat<State>) {
|
|||
state.backend.schedule_render(&output);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{HideReason, PointerEventKind};
|
||||
|
||||
#[test]
|
||||
fn touch_hidden_cursor_waits_for_movement() {
|
||||
assert!(HideReason::Touch.revealed_by(PointerEventKind::Motion));
|
||||
assert!(!HideReason::Touch.revealed_by(PointerEventKind::Other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_and_typing_hidden_cursors_are_revealed_by_any_pointer_event() {
|
||||
for reason in [HideReason::Idle, HideReason::Typing] {
|
||||
assert!(reason.revealed_by(PointerEventKind::Motion));
|
||||
// A click or a scroll must rescue the cursor rather than forcing a
|
||||
// blind click.
|
||||
assert!(reason.revealed_by(PointerEventKind::Other));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1013,8 +1013,11 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
|
|||
let seats: Vec<_> = state.common.shell.read().seats.iter().cloned().collect();
|
||||
let mut needs_render = false;
|
||||
for seat in seats {
|
||||
needs_render |=
|
||||
crate::backend::render::cursor::notify_cursor_activity(state, &seat);
|
||||
needs_render |= crate::backend::render::cursor::notify_cursor_activity(
|
||||
state,
|
||||
&seat,
|
||||
crate::backend::render::cursor::PointerEventKind::Motion,
|
||||
);
|
||||
}
|
||||
if needs_render {
|
||||
let outputs: Vec<_> =
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
backend::render::{ElementFilter, cursor::notify_cursor_activity},
|
||||
backend::render::{
|
||||
ElementFilter,
|
||||
cursor::{PointerEventKind, notify_cursor_activity},
|
||||
},
|
||||
config::{
|
||||
Action, Config, PrivateAction,
|
||||
key_bindings::{
|
||||
|
|
@ -349,7 +352,7 @@ impl State {
|
|||
.cloned()
|
||||
{
|
||||
self.common.idle_notifier_state.notify_activity(&seat);
|
||||
notify_cursor_activity(self, &seat);
|
||||
notify_cursor_activity(self, &seat, PointerEventKind::Motion);
|
||||
let current_output = seat.active_output();
|
||||
|
||||
if self.common.config.cosmic_conf.cursor_shake_to_find
|
||||
|
|
@ -723,7 +726,7 @@ impl State {
|
|||
.cloned();
|
||||
if let Some(seat) = maybe_seat {
|
||||
self.common.idle_notifier_state.notify_activity(&seat);
|
||||
notify_cursor_activity(self, &seat);
|
||||
notify_cursor_activity(self, &seat, PointerEventKind::Motion);
|
||||
let (output, position) = if matches!(&backend_id, InputBackendId::Ei(_)) {
|
||||
// EI absolute coordinates are in the compositor's *global*
|
||||
// logical space: each advertised region carries its output's
|
||||
|
|
@ -818,7 +821,7 @@ impl State {
|
|||
return;
|
||||
};
|
||||
self.common.idle_notifier_state.notify_activity(&seat);
|
||||
notify_cursor_activity(self, &seat);
|
||||
notify_cursor_activity(self, &seat, PointerEventKind::Other);
|
||||
|
||||
let current_focus = seat.get_keyboard().unwrap().current_focus();
|
||||
let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| {
|
||||
|
|
@ -1069,7 +1072,7 @@ impl State {
|
|||
.cloned();
|
||||
if let Some(seat) = maybe_seat {
|
||||
self.common.idle_notifier_state.notify_activity(&seat);
|
||||
notify_cursor_activity(self, &seat);
|
||||
notify_cursor_activity(self, &seat, PointerEventKind::Other);
|
||||
|
||||
if self.source_modifiers(&backend_id, &seat).logo
|
||||
&& self
|
||||
|
|
@ -1608,7 +1611,7 @@ impl State {
|
|||
.cloned()
|
||||
{
|
||||
self.common.idle_notifier_state.notify_activity(&seat);
|
||||
notify_cursor_activity(self, &seat);
|
||||
notify_cursor_activity(self, &seat, PointerEventKind::Motion);
|
||||
let Some(output) =
|
||||
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
||||
.cloned()
|
||||
|
|
@ -1712,7 +1715,7 @@ impl State {
|
|||
.cloned()
|
||||
{
|
||||
self.common.idle_notifier_state.notify_activity(&seat);
|
||||
notify_cursor_activity(self, &seat);
|
||||
notify_cursor_activity(self, &seat, PointerEventKind::Motion);
|
||||
let Some(output) =
|
||||
mapped_output_for_device(&self.common.config, &shell, &event.device())
|
||||
.cloned()
|
||||
|
|
@ -1861,7 +1864,7 @@ impl State {
|
|||
.cloned();
|
||||
if let Some(seat) = maybe_seat {
|
||||
self.common.idle_notifier_state.notify_activity(&seat);
|
||||
notify_cursor_activity(self, &seat);
|
||||
notify_cursor_activity(self, &seat, PointerEventKind::Other);
|
||||
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
let output = seat.active_output();
|
||||
|
|
@ -1914,7 +1917,7 @@ impl State {
|
|||
.cloned();
|
||||
if let Some(seat) = maybe_seat {
|
||||
self.common.idle_notifier_state.notify_activity(&seat);
|
||||
notify_cursor_activity(self, &seat);
|
||||
notify_cursor_activity(self, &seat, PointerEventKind::Other);
|
||||
if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) {
|
||||
tool.button(
|
||||
self,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue