diff --git a/Cargo.toml b/Cargo.toml index 43af617..e8bc6e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ thiserror = "1.0.2" udev = { version = "0.6", optional = true } wayland-commons = { version = "0.29.0", optional = true } wayland-egl = { version = "0.29.0", optional = true } -wayland-protocols = { version = "0.29.0", features = ["unstable_protocols", "server"], optional = true } +wayland-protocols = { version = "0.29.0", features = ["unstable_protocols", "staging_protocols", "server"], optional = true } wayland-server = { version = "0.29.0", optional = true } wayland-sys = { version = "0.29.0", optional = true } winit = { version = "0.25.0", optional = true } diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index f14c7a2..d9810ca 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -63,6 +63,7 @@ pub mod seat; pub mod shell; pub mod shm; pub mod tablet_manager; +pub mod xdg_activation; pub mod xdg_foreign; /// A global [`SerialCounter`] for use in your compositor. diff --git a/src/wayland/xdg_activation/handlers.rs b/src/wayland/xdg_activation/handlers.rs new file mode 100644 index 0000000..70f03b5 --- /dev/null +++ b/src/wayland/xdg_activation/handlers.rs @@ -0,0 +1,147 @@ +use std::{ + cell::RefCell, + rc::Rc, + sync::{Arc, Mutex}, +}; + +use wayland_protocols::staging::xdg_activation::v1::server::{xdg_activation_token_v1, xdg_activation_v1}; +use wayland_server::{ + protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, + DispatchData, Filter, Main, +}; + +use crate::wayland::Serial; + +use super::{XdgActivationEvent, XdgActivationState, XdgActivationToken, XdgActivationTokenData}; + +type Impl = dyn FnMut(&Mutex, XdgActivationEvent, DispatchData<'_>); + +/// New xdg activation global +pub(super) fn implement_activation_global( + global: Main, + state: Arc>, + implementation: Rc>, +) { + global.quick_assign(move |_, req, ddata| match req { + xdg_activation_v1::Request::GetActivationToken { id } => { + get_activation_token(id, state.clone(), implementation.clone()); + } + xdg_activation_v1::Request::Activate { token, surface } => { + activate( + token.into(), + surface, + state.as_ref(), + implementation.as_ref(), + ddata, + ); + } + _ => {} + }); +} + +/// New xdg activation token +fn get_activation_token( + id: Main, + state: Arc>, + implementation: Rc>, +) { + id.quick_assign({ + let state = state.clone(); + + let mut token_serial: Option<(Serial, WlSeat)> = None; + let mut token_app_id: Option = None; + let mut token_surface: Option = None; + let mut token_constructed = false; + + move |id, req, _| { + if !token_constructed { + match req { + xdg_activation_token_v1::Request::SetSerial { serial, seat } => { + token_serial = Some((serial.into(), seat)); + } + xdg_activation_token_v1::Request::SetAppId { app_id } => { + token_app_id = Some(app_id); + } + xdg_activation_token_v1::Request::SetSurface { surface } => { + token_surface = Some(surface); + } + xdg_activation_token_v1::Request::Commit => { + let (token, token_data) = XdgActivationTokenData::new( + token_serial.take(), + token_app_id.take(), + token_surface.take(), + ); + + state + .lock() + .unwrap() + .pending_tokens + .insert(token.clone(), token_data); + id.as_ref().user_data().set_threadsafe(|| token.clone()); + + id.done(token.to_string()); + + token_constructed = true; + } + _ => {} + }; + } else { + id.as_ref().post_error( + xdg_activation_token_v1::Error::AlreadyUsed as u32, + "The activation token has already been constructed".into(), + ) + } + } + }); + + id.assign_destructor(Filter::new( + move |token: xdg_activation_token_v1::XdgActivationTokenV1, _, ddata| { + if let Some(token) = token.as_ref().user_data().get::() { + state.lock().unwrap().pending_tokens.remove(token); + + if let Some((token_data, surface)) = state.lock().unwrap().activation_requests.remove(token) { + let mut cb = implementation.borrow_mut(); + cb( + &state, + XdgActivationEvent::DestroyActivationRequest { + token: token.clone(), + token_data, + surface, + }, + ddata, + ); + } + } + }, + )); +} + +/// Xdg activation request +fn activate( + token: XdgActivationToken, + surface: WlSurface, + state: &Mutex, + implementation: &RefCell, + ddata: DispatchData<'_>, +) { + let mut guard = state.lock().unwrap(); + if let Some(token_data) = guard.pending_tokens.remove(&token) { + guard + .activation_requests + .insert(token.clone(), (token_data.clone(), surface.clone())); + + // The user may want to use state, so we need to unlock it + drop(guard); + + let mut cb = implementation.borrow_mut(); + cb( + state, + XdgActivationEvent::RequestActivation { + token: token.clone(), + token_data, + surface, + }, + ddata, + ); + } +} diff --git a/src/wayland/xdg_activation/mod.rs b/src/wayland/xdg_activation/mod.rs new file mode 100644 index 0000000..121e116 --- /dev/null +++ b/src/wayland/xdg_activation/mod.rs @@ -0,0 +1,257 @@ +//! Utilities for handling activation requests with the `xdg_activation` protocol +//! +//! +//! ### Example +//! ```no_run +//! # extern crate wayland_server; +//! # +//! use smithay::wayland::xdg_activation::{init_xdg_activation_global, XdgActivationEvent}; +//! +//! # let mut display = wayland_server::Display::new(); +//! let (state, _) = init_xdg_activation_global( +//! &mut display, +//! // your implementation +//! |state, req, dispatch_data| { +//! match req{ +//! XdgActivationEvent::RequestActivation { token, token_data, surface } => { +//! if token_data.timestamp.elapsed().as_secs() < 10 { +//! // Request surface activation +//! } else{ +//! // Discard the request +//! state.lock().unwrap().remove_request(&token); +//! } +//! }, +//! XdgActivationEvent::DestroyActivationRequest {..} => { +//! // The request is canceled +//! }, +//! } +//! }, +//! None // put a logger if you want +//! ); +//! ``` + +use std::{ + cell::RefCell, + collections::HashMap, + ops, + rc::Rc, + sync::{Arc, Mutex}, + time::Instant, +}; + +use wayland_protocols::staging::xdg_activation::v1::server::xdg_activation_v1; +use wayland_server::{ + protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, + DispatchData, Display, Filter, Global, Main, UserDataMap, +}; + +use rand::distributions::{Alphanumeric, DistString}; + +use crate::wayland::Serial; + +mod handlers; + +/// Contains the unique string token of activation request +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct XdgActivationToken(String); + +impl XdgActivationToken { + fn new() -> Self { + Self(Alphanumeric.sample_string(&mut rand::thread_rng(), 32)) + } + + /// Extracts a string slice containing the entire token. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl ops::Deref for XdgActivationToken { + type Target = str; + #[inline] + fn deref(&self) -> &str { + &self.0 + } +} + +impl From for XdgActivationToken { + fn from(s: String) -> Self { + Self(s) + } +} + +impl From for String { + fn from(s: XdgActivationToken) -> Self { + s.0 + } +} + +/// Activation data asosiated with the [`XdgActivationToken`] + +#[derive(Debug, Clone)] +pub struct XdgActivationTokenData { + /// Provides information about the seat and serial event that requested the token. + /// + /// The serial can come from an input or focus event. + /// For instance, if a click triggers the launch of a third-party client, + /// this field should contain serial and seat from the wl_pointer.button event. + /// + /// Some compositors might refuse to activate toplevels + /// when the token doesn't have a valid and recent enough event serial. + pub serial: Option<(Serial, WlSeat)>, + /// The requesting client can specify an app_id to associate the token being created with it. + pub app_id: Option, + /// The surface requesting the activation. + /// + /// Note, this is different from the surface that will be activated. + pub surface: Option, + /// Timestamp of the token + /// + /// You can use this do ignore tokens based on time. + /// For example you coould ignore all tokens older that 5s. + pub timestamp: Instant, +} + +impl XdgActivationTokenData { + fn new( + serial: Option<(Serial, WlSeat)>, + app_id: Option, + surface: Option, + ) -> (XdgActivationToken, XdgActivationTokenData) { + ( + XdgActivationToken::new(), + XdgActivationTokenData { + serial, + app_id, + surface, + timestamp: Instant::now(), + }, + ) + } +} + +/// Tracks the list of pending and current activation requests +#[derive(Debug)] +pub struct XdgActivationState { + log: ::slog::Logger, + user_data: UserDataMap, + + pending_tokens: HashMap, + + activation_requests: HashMap, +} + +impl XdgActivationState { + /// Get current activation requests + /// + /// HashMap contains token data and target surface + pub fn requests(&self) -> &HashMap { + &self.activation_requests + } + + /// Remove and return the activation request + /// + /// If you consider a request to be unwanted you can use this method to + /// discard it and don't track it any futher. + pub fn remove_request( + &mut self, + token: &XdgActivationToken, + ) -> Option<(XdgActivationTokenData, WlSurface)> { + self.activation_requests.remove(token) + } + + /// Retain activation requests + pub fn retain_reqests(&mut self, mut f: F) + where + F: FnMut(&XdgActivationToken, &(XdgActivationTokenData, WlSurface)) -> bool, + { + self.activation_requests.retain(|k, v| f(k, v)) + } + + /// Retain pending tokens + /// + /// You may want to remove super old tokens + /// that were never turned into activation request for some reason + pub fn retain_pending_tokens(&mut self, mut f: F) + where + F: FnMut(&XdgActivationToken, &XdgActivationTokenData) -> bool, + { + self.pending_tokens.retain(|k, v| f(k, v)) + } + + /// Access the `UserDataMap` associated with this `XdgActivationState ` + pub fn user_data(&self) -> &UserDataMap { + &self.user_data + } +} + +/// Creates new `xdg-activation` global. +pub fn init_xdg_activation_global( + display: &mut Display, + implementation: Impl, + logger: L, +) -> ( + Arc>, + Global, +) +where + L: Into>, + Impl: FnMut(&Mutex, XdgActivationEvent, DispatchData<'_>) + 'static, +{ + let log = crate::slog_or_fallback(logger); + + let implementation = Rc::new(RefCell::new(implementation)); + + let activation_state = Arc::new(Mutex::new(XdgActivationState { + log: log.new(slog::o!("smithay_module" => "xdg_activation_handler")), + user_data: UserDataMap::new(), + pending_tokens: HashMap::new(), + activation_requests: HashMap::new(), + })); + + let state = activation_state.clone(); + let global = display.create_global( + 1, + Filter::new( + move |(global, _version): (Main, _), _, _| { + handlers::implement_activation_global(global, state.clone(), implementation.clone()); + }, + ), + ); + + (activation_state, global) +} + +/// Xdg activation related events +#[derive(Debug)] +pub enum XdgActivationEvent { + /// Requests surface activation. + /// + /// The compositor may know who requested this by checking the token data + /// and might decide not to follow through with the activation if it's considered unwanted. + /// + /// If you consider a request to be unwanted you can use [`XdgActivationState::remove_request`] + /// to discard it and don't track it any futher. + RequestActivation { + /// Token of the request + token: XdgActivationToken, + /// Data asosiated with the token + token_data: XdgActivationTokenData, + /// Target surface + surface: WlSurface, + }, + /// The activation token just got destroyed + /// + /// In response to that activation request should be canceled. + /// + /// For example if your compostior blinks a window when it requests activation, + /// after this request the animation should stop. + DestroyActivationRequest { + /// Token of the request that just died + token: XdgActivationToken, + /// Data asosiated with the token + token_data: XdgActivationTokenData, + /// Target surface + surface: WlSurface, + }, +}