//! This interface allows clients to monitor user idle status. //! //! ``` //! # extern crate wayland_server; //! # #[macro_use] extern crate smithay; //! use smithay::delegate_idle_notify; //! use smithay::wayland::idle_notify::{IdleNotifierState, IdleNotifierHandler}; //! # use smithay::input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus}; //! # use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; //! //! struct State { idle_notifier: IdleNotifierState } //! # let mut event_loop = smithay::reexports::calloop::EventLoop::::try_new().unwrap(); //! # let mut display = wayland_server::Display::::new().unwrap(); //! // Create the idle_notifier state //! let idle_notifier = IdleNotifierState::::new( //! &display.handle(), //! event_loop.handle(), //! ); //! //! let state = State { idle_notifier }; //! //! // Implement the necessary trait //! # impl SeatHandler for State { //! # type KeyboardFocus = WlSurface; //! # type PointerFocus = WlSurface; //! # type TouchFocus = WlSurface; //! # fn seat_state(&mut self) -> &mut SeatState { unimplemented!() } //! # fn focus_changed(&mut self, seat: &Seat, focused: Option<&WlSurface>) { unimplemented!() } //! # fn cursor_image(&mut self, seat: &Seat, image: CursorImageStatus) { unimplemented!() } //! # } //! impl IdleNotifierHandler for State { //! fn idle_notifier_state(&mut self) -> &mut IdleNotifierState { //! &mut self.idle_notifier //! } //! } //! delegate_idle_notify!(State); //! //! // On input you should notify the idle_notifier //! // state.idle_notifier.notify_activity(&seat); //! ``` use std::{ collections::HashMap, sync::{ atomic::{self, AtomicBool}, Mutex, }, time::Duration, }; use calloop::{timer::TimeoutAction, LoopHandle, RegistrationToken}; use wayland_protocols::ext::idle_notify::v1::server::{ ext_idle_notification_v1::{self, ExtIdleNotificationV1}, ext_idle_notifier_v1::{self, ExtIdleNotifierV1}, }; use wayland_server::{ backend::{ClientId, GlobalId}, protocol::wl_seat::WlSeat, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, }; use crate::input::{Seat, SeatHandler}; /// Handler trait for ext-idle-notify pub trait IdleNotifierHandler: Sized { /// [`IdleNotifierState`] getter fn idle_notifier_state(&mut self) -> &mut IdleNotifierState; } /// User data of the [`ExtIdleNotificationV1`] resource #[derive(Debug)] pub struct IdleNotificationUserData { seat: WlSeat, is_idle: AtomicBool, timeout: Duration, timer_token: Mutex>, /// If listener was created with `get_input_idle_notification` ignore_inhibitor: bool, } impl IdleNotificationUserData { fn take_timer_token(&self) -> Option { self.timer_token.lock().unwrap().take() } fn set_timer_token(&self, idle: Option) { *self.timer_token.lock().unwrap() = idle; } fn set_idle(&self, idle: bool) { self.is_idle.store(idle, atomic::Ordering::Release); } fn is_idle(&self) -> bool { self.is_idle.load(atomic::Ordering::Acquire) } } /// State of ext-idle-notify module #[derive(Debug)] pub struct IdleNotifierState { global: GlobalId, notifications: HashMap>, loop_handle: LoopHandle<'static, D>, is_inhibited: bool, } impl IdleNotifierState { /// Create new [`ExtIdleNotifierV1`] global. pub fn new(display: &DisplayHandle, loop_handle: LoopHandle<'static, D>) -> Self where D: GlobalDispatch, D: Dispatch, D: Dispatch, D: IdleNotifierHandler, D: 'static, { let global = display.create_global::(2, ()); Self { global, notifications: HashMap::new(), loop_handle, is_inhibited: false, } } /// Inhibit entering idle state, eg. by the idle-inhibit protocol pub fn set_is_inhibited(&mut self, is_inhibited: bool) { if self.is_inhibited == is_inhibited { return; } self.is_inhibited = is_inhibited; for notification in self.notifications() { let data = notification.data::().unwrap(); if data.ignore_inhibitor { continue; } if is_inhibited { if data.is_idle() { notification.resumed(); data.set_idle(false); } if let Some(token) = data.take_timer_token() { self.loop_handle.remove(token); } } else { self.reinsert_timer(notification); } } } /// Is idle state inhibited, eg. by the idle-inhibit protocol pub fn is_inhibited(&mut self) -> bool { self.is_inhibited } /// Should be called whenever activity occurs on a seat, eg. mouse/keyboard input. /// /// You may want to use [`Self::notify_activity`] instead which accepts a [`Seat`]. pub fn notify_activity_for_wl_seat(&mut self, seat: &WlSeat) { let Some(notifications) = self.notifications.get(seat) else { return; }; for notification in notifications { let data = notification.data::().unwrap(); if data.is_idle() { notification.resumed(); data.set_idle(false); } self.reinsert_timer(notification); } } /// Returns the [`ExtIdleNotifierV1`] global. pub fn global(&self) -> GlobalId { self.global.clone() } fn notifications(&self) -> impl Iterator { self.notifications.values().flatten() } fn reinsert_timer(&self, notification: &ExtIdleNotificationV1) { let data = notification.data::().unwrap(); if let Some(token) = data.take_timer_token() { self.loop_handle.remove(token); } if !data.ignore_inhibitor && self.is_inhibited { return; } let token = self .loop_handle .insert_source(calloop::timer::Timer::from_duration(data.timeout), { let idle_notification = notification.clone(); move |_, _, state| { let data = idle_notification.data::().unwrap(); let is_inhibited = !data.ignore_inhibitor && state.idle_notifier_state().is_inhibited; let is_idle_already = data.is_idle(); if !is_inhibited && !is_idle_already { idle_notification.idled(); data.set_idle(true); } data.set_timer_token(None); TimeoutAction::Drop } }); data.set_timer_token(token.ok()); } } impl IdleNotifierState { /// Should be called whenever activity occurs on a seat, eg. mouse/keyboard input. pub fn notify_activity(&mut self, seat: &Seat) { for seat in &seat.arc.inner.lock().unwrap().known_seats { if let Ok(seat) = seat.upgrade() { self.notify_activity_for_wl_seat(&seat); } } } } impl GlobalDispatch for IdleNotifierState where D: GlobalDispatch, D: Dispatch, D: Dispatch, D: IdleNotifierHandler, D: 'static, { fn bind( _state: &mut D, _handle: &DisplayHandle, _client: &Client, resource: New, _global_data: &(), data_init: &mut wayland_server::DataInit<'_, D>, ) { data_init.init(resource, ()); } } impl Dispatch for IdleNotifierState where D: GlobalDispatch, D: Dispatch, D: Dispatch, D: IdleNotifierHandler, D: 'static, { fn request( state: &mut D, _client: &Client, _resource: &ExtIdleNotifierV1, request: ext_idle_notifier_v1::Request, _data: &(), _dhandle: &DisplayHandle, data_init: &mut DataInit<'_, D>, ) { match request { ext_idle_notifier_v1::Request::GetIdleNotification { id, timeout, seat } => { let timeout = Duration::from_millis(timeout as u64); let idle_notifier_state = state.idle_notifier_state(); let idle_notification = data_init.init( id, IdleNotificationUserData { seat: seat.clone(), is_idle: AtomicBool::new(false), timeout, timer_token: Mutex::new(None), ignore_inhibitor: false, }, ); idle_notifier_state.reinsert_timer(&idle_notification); state .idle_notifier_state() .notifications .entry(seat) .or_default() .push(idle_notification); } ext_idle_notifier_v1::Request::GetInputIdleNotification { id, timeout, seat } => { let timeout = Duration::from_millis(timeout as u64); let idle_notifier_state = state.idle_notifier_state(); let idle_notification = data_init.init( id, IdleNotificationUserData { seat: seat.clone(), is_idle: AtomicBool::new(false), timeout, timer_token: Mutex::new(None), ignore_inhibitor: true, }, ); idle_notifier_state.reinsert_timer(&idle_notification); state .idle_notifier_state() .notifications .entry(seat) .or_default() .push(idle_notification); } ext_idle_notifier_v1::Request::Destroy => {} _ => unimplemented!(), } } } impl Dispatch for IdleNotifierState where D: Dispatch, D: IdleNotifierHandler, { fn request( _state: &mut D, _client: &Client, _resource: &ExtIdleNotificationV1, request: ext_idle_notification_v1::Request, _data: &IdleNotificationUserData, _dhandle: &DisplayHandle, _data_init: &mut DataInit<'_, D>, ) { match request { ext_idle_notification_v1::Request::Destroy => {} _ => unimplemented!(), } } fn destroyed( state: &mut D, _client: ClientId, notification: &ExtIdleNotificationV1, data: &IdleNotificationUserData, ) { let state = state.idle_notifier_state(); if let Some(notifications) = state.notifications.get_mut(&data.seat) { notifications.retain(|x| x != notification); } state .notifications .retain(|seat, notifications| !notifications.is_empty() && seat.is_alive()); } } /// Macro to delegate implementation of the ext idle notify protocol #[macro_export] macro_rules! delegate_idle_notify { ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { type __ExtIdleNotifierV1 = $crate::reexports::wayland_protocols::ext::idle_notify::v1::server::ext_idle_notifier_v1::ExtIdleNotifierV1; type __ExtIdleNotificationV1 = $crate::reexports::wayland_protocols::ext::idle_notify::v1::server::ext_idle_notification_v1::ExtIdleNotificationV1; $crate::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ __ExtIdleNotifierV1: () ] => $crate::wayland::idle_notify::IdleNotifierState<$ty> ); $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ __ExtIdleNotifierV1: () ] => $crate::wayland::idle_notify::IdleNotifierState<$ty> ); $crate::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ __ExtIdleNotificationV1: $crate::wayland::idle_notify::IdleNotificationUserData ] => $crate::wayland::idle_notify::IdleNotifierState<$ty> ); }; }