From f7a84a5b50aca5a00419ae2039e947f7b63b794d Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Thu, 20 Jul 2023 13:16:51 +0000 Subject: [PATCH] Add `platform::startup_notify` for Wayland/X11 The utils in this module should help the users to activate the windows they create, as well as manage activation tokens environment variables. The API is essential for Wayland in the first place, since some compositors may decide initial focus of the window based on whether the activation token was during the window creation. Fixes #2279. Co-authored-by: John Nunley --- CHANGELOG.md | 1 + Cargo.toml | 5 +- examples/startup_notification.rs | 127 +++++++++++ src/event.rs | 22 +- src/event_loop.rs | 28 ++- src/platform/mod.rs | 2 + src/platform/startup_notify.rs | 109 ++++++++++ src/platform_impl/linux/mod.rs | 16 +- .../linux/wayland/types/xdg_activation.rs | 51 ++--- src/platform_impl/linux/wayland/window/mod.rs | 31 ++- src/platform_impl/linux/x11/activation.rs | 200 ++++++++++++++++++ src/platform_impl/linux/x11/atoms.rs | 5 + .../linux/x11/event_processor.rs | 60 +++++- src/platform_impl/linux/x11/mod.rs | 60 ++++++ src/platform_impl/linux/x11/window.rs | 36 ++++ src/platform_impl/linux/x11/xdisplay.rs | 39 +++- src/window.rs | 14 ++ 17 files changed, 771 insertions(+), 35 deletions(-) create mode 100644 examples/startup_notification.rs create mode 100644 src/platform/startup_notify.rs create mode 100644 src/platform_impl/linux/x11/activation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e7819823..65bd9502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- **Breaking:** `ActivationTokenDone` event which could be requested with the new `startup_notify` module, see its docs for more. - On Wayland, make double clicking and moving the CSD frame more reliable. - On macOS, add tabbing APIs on `WindowExtMacOS` and `EventLoopWindowTargetExtMacOS`. - **Breaking:** Rename `Window::set_inner_size` to `Window::request_inner_size` and indicate if the size was applied immediately. diff --git a/Cargo.toml b/Cargo.toml index 5c5b9472..cfaf593b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -x11 = ["x11-dl", "bytemuck", "percent-encoding", "xkbcommon-dl/x11", "x11rb"] +x11 = ["x11-dl", "bytemuck", "rustix", "percent-encoding", "xkbcommon-dl/x11", "x11rb"] wayland = ["wayland-client", "wayland-backend", "wayland-protocols", "sctk", "fnv", "memmap2"] wayland-dlopen = ["wayland-backend/dlopen"] wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/ab_glyph"] @@ -55,7 +55,7 @@ cursor-icon = "1.0.0" log = "0.4" mint = { version = "0.5.6", optional = true } once_cell = "1.12" -raw_window_handle = { package = "raw-window-handle", version = "0.5" } +raw_window_handle = { package = "raw-window-handle", version = "0.5", features = ["std"] } serde = { version = "1", optional = true, features = ["serde_derive"] } smol_str = "0.2.0" @@ -123,6 +123,7 @@ wayland-client = { version = "0.30.0", optional = true } wayland-backend = { version = "0.1.0", default_features = false, features = ["client_system"], optional = true } wayland-protocols = { version = "0.30.0", features = [ "staging"], optional = true } calloop = "0.10.5" +rustix = { version = "0.38.4", default-features = false, features = ["std", "system", "process"], optional = true } x11-dl = { version = "2.18.5", optional = true } x11rb = { version = "0.12.0", default-features = false, features = ["allow-unsafe-code", "dl-libxcb", "xinput", "xkb"], optional = true } xkbcommon-dl = "0.4.0" diff --git a/examples/startup_notification.rs b/examples/startup_notification.rs new file mode 100644 index 00000000..5de61401 --- /dev/null +++ b/examples/startup_notification.rs @@ -0,0 +1,127 @@ +//! Demonstrates the use of startup notifications on Linux. + +fn main() { + example::main(); +} + +#[cfg(any(x11_platform, wayland_platform))] +#[path = "./util/fill.rs"] +mod fill; + +#[cfg(any(x11_platform, wayland_platform))] +mod example { + use std::collections::HashMap; + use std::rc::Rc; + + use winit::event::{ElementState, Event, KeyEvent, WindowEvent}; + use winit::event_loop::EventLoop; + use winit::keyboard::Key; + use winit::platform::startup_notify::{ + EventLoopExtStartupNotify, WindowBuilderExtStartupNotify, WindowExtStartupNotify, + }; + use winit::window::{Window, WindowBuilder, WindowId}; + + pub(super) fn main() { + // Create the event loop and get the activation token. + let event_loop = EventLoop::new(); + let mut current_token = match event_loop.read_token_from_env() { + Some(token) => Some(token), + None => { + println!("No startup notification token found in environment."); + None + } + }; + + let mut windows: HashMap> = HashMap::new(); + let mut counter = 0; + let mut create_first_window = false; + + event_loop.run(move |event, elwt, flow| { + match event { + Event::Resumed => create_first_window = true, + + Event::WindowEvent { + window_id, + event: + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key, + state: ElementState::Pressed, + .. + }, + .. + }, + } => { + if logical_key == Key::Character("n".into()) { + if let Some(window) = windows.get(&window_id) { + // Request a new activation token on this window. + // Once we get it we will use it to create a window. + window + .request_activation_token() + .expect("Failed to request activation token."); + } + } + } + + Event::WindowEvent { + window_id, + event: WindowEvent::CloseRequested, + } => { + // Remove the window from the map. + windows.remove(&window_id); + if windows.is_empty() { + flow.set_exit(); + return; + } + } + + Event::WindowEvent { + event: WindowEvent::ActivationTokenDone { token, .. }, + .. + } => { + current_token = Some(token); + } + + Event::RedrawRequested(id) => { + if let Some(window) = windows.get(&id) { + super::fill::fill_window(window); + } + } + + _ => {} + } + + // See if we've passed the deadline. + if current_token.is_some() || create_first_window { + // Create the initial window. + let window = { + let mut builder = + WindowBuilder::new().with_title(format!("Window {}", counter)); + + if let Some(token) = current_token.take() { + println!("Creating a window with token {token:?}"); + builder = builder.with_activation_token(token); + } + + Rc::new(builder.build(elwt).unwrap()) + }; + + // Add the window to the map. + windows.insert(window.id(), window.clone()); + + counter += 1; + create_first_window = false; + } + + flow.set_wait(); + }); + } +} + +#[cfg(not(any(x11_platform, wayland_platform)))] +mod example { + pub(super) fn main() { + println!("This example is only supported on X11 and Wayland platforms."); + } +} diff --git a/src/event.rs b/src/event.rs index e419110a..eccc347f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -45,9 +45,10 @@ use web_time::Instant; use crate::window::Window; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, + event_loop::AsyncRequestSerial, keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}, platform_impl, - window::{Theme, WindowId}, + window::{ActivationToken, Theme, WindowId}, }; /// Describes a generic event. @@ -356,6 +357,20 @@ pub enum StartCause { /// Describes an event from a [`Window`]. #[derive(Debug, PartialEq)] pub enum WindowEvent<'a> { + /// The activation token was delivered back and now could be used. + /// + #[cfg_attr( + not(any(x11_platform, wayland_platfrom)), + allow(rustdoc::broken_intra_doc_links) + )] + /// Delivered in response to [`request_activation_token`]. + /// + /// [`request_activation_token`]: crate::platform::startup_notify::WindowExtStartupNotify::request_activation_token + ActivationTokenDone { + serial: AsyncRequestSerial, + token: ActivationToken, + }, + /// The size of the window has changed. Contains the client area's new dimensions. Resized(PhysicalSize), @@ -608,6 +623,10 @@ impl Clone for WindowEvent<'static> { fn clone(&self) -> Self { use self::WindowEvent::*; return match self { + ActivationTokenDone { serial, token } => ActivationTokenDone { + serial: *serial, + token: token.clone(), + }, Resized(size) => Resized(*size), Moved(pos) => Moved(*pos), CloseRequested => CloseRequested, @@ -711,6 +730,7 @@ impl<'a> WindowEvent<'a> { pub fn to_static(self) -> Option> { use self::WindowEvent::*; match self { + ActivationTokenDone { serial, token } => Some(ActivationTokenDone { serial, token }), Resized(size) => Some(Resized(size)), Moved(position) => Some(Moved(position)), CloseRequested => Some(CloseRequested), diff --git a/src/event_loop.rs b/src/event_loop.rs index f8e373ed..1485069d 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -9,7 +9,7 @@ //! handle events. use std::marker::PhantomData; use std::ops::Deref; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::{error, fmt}; use raw_window_handle::{HasRawDisplayHandle, RawDisplayHandle}; @@ -437,3 +437,29 @@ pub enum DeviceEvents { /// Never capture device events. Never, } + +/// A unique identifier of the winit's async request. +/// +/// This could be used to identify the async request once it's done +/// and a specific action must be taken. +/// +/// One of the handling scenarious could be to maintain a working list +/// containing [`AsyncRequestSerial`] and some closure associated with it. +/// Then once event is arriving the working list is being traversed and a job +/// executed and removed from the list. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AsyncRequestSerial { + serial: u64, +} + +impl AsyncRequestSerial { + // TODO(kchibisov) remove `cfg` when the clipboard will be added. + #[allow(dead_code)] + pub(crate) fn get() -> Self { + static CURRENT_SERIAL: AtomicU64 = AtomicU64::new(0); + // NOTE: we rely on wrap around here, while the user may just request + // in the loop u64::MAX times that's issue is considered on them. + let serial = CURRENT_SERIAL.fetch_add(1, Ordering::Relaxed); + Self { serial } + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 1985b7d7..9e01c4d9 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -23,6 +23,8 @@ pub mod ios; pub mod macos; #[cfg(orbital_platform)] pub mod orbital; +#[cfg(any(x11_platform, wayland_platform))] +pub mod startup_notify; #[cfg(wayland_platform)] pub mod wayland; #[cfg(wasm_platform)] diff --git a/src/platform/startup_notify.rs b/src/platform/startup_notify.rs new file mode 100644 index 00000000..2548be98 --- /dev/null +++ b/src/platform/startup_notify.rs @@ -0,0 +1,109 @@ +//! Window startup notification to handle window raising. +//! +//! The [`ActivationToken`] is essential to ensure that your newly +//! created window will obtain the focus, otherwise the user could +//! be requered to click on the window. +//! +//! Such token is usually delivered via the environment variable and +//! could be read from it with the [`EventLoopExtStartupNotify::read_token_from_env`]. +//! +//! Such token must also be reset after reading it from your environment with +//! [`reset_activation_token_env`] otherwise child processes could inherit it. +//! +//! When starting a new child process with a newly obtained [`ActivationToken`] from +//! [`WindowExtStartupNotify::request_activation_token`] the [`set_activation_token_env`] +//! must be used to propagate it to the child +//! +//! To ensure the delivery of such token by other processes to you, the user should +//! set `StartupNotify=true` inside the `.desktop` file of their application. +//! +//! The specification could be found [`here`]. +//! +//! [`here`]: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-latest.txt + +use std::env; + +use crate::error::NotSupportedError; +use crate::event_loop::{AsyncRequestSerial, EventLoopWindowTarget}; +use crate::window::{ActivationToken, Window, WindowBuilder}; + +/// The variable which is used mostly on X11. +const X11_VAR: &str = "DESKTOP_STARTUP_ID"; + +/// The variable which is used mostly on Wayland. +const WAYLAND_VAR: &str = "XDG_ACTIVATION_TOKEN"; + +pub trait EventLoopExtStartupNotify { + /// Read the token from the environment. + /// + /// It's recommended **to unset** this environment variable for child processes. + fn read_token_from_env(&self) -> Option; +} + +pub trait WindowExtStartupNotify { + /// Request a new activation token. + /// + /// The token will be delivered inside + fn request_activation_token(&self) -> Result; +} + +pub trait WindowBuilderExtStartupNotify { + /// Use this [`ActivationToken`] during window creation. + /// + /// Not using such a token upon a window could make your window not gaining + /// focus until the user clicks on the window. + fn with_activation_token(self, token: ActivationToken) -> Self; +} + +impl EventLoopExtStartupNotify for EventLoopWindowTarget { + fn read_token_from_env(&self) -> Option { + match self.p { + #[cfg(wayland_platform)] + crate::platform_impl::EventLoopWindowTarget::Wayland(_) => env::var(WAYLAND_VAR), + #[cfg(x11_platform)] + crate::platform_impl::EventLoopWindowTarget::X(_) => env::var(X11_VAR), + } + .ok() + .map(ActivationToken::_new) + } +} + +impl WindowExtStartupNotify for Window { + fn request_activation_token(&self) -> Result { + self.window.request_activation_token() + } +} + +impl WindowBuilderExtStartupNotify for WindowBuilder { + fn with_activation_token(mut self, token: ActivationToken) -> Self { + self.platform_specific.activation_token = Some(token); + self + } +} + +/// Remove the activation environment variables from the current process. +/// +/// This is wise to do before running child processes, +/// which may not to support the activation token. +/// +/// # Safety +/// +/// While the function is safe internally, it mutates the global environment +/// state for the process, hence unsafe. +pub unsafe fn reset_activation_token_env() { + env::remove_var(X11_VAR); + env::remove_var(WAYLAND_VAR); +} + +/// Set environment variables responsible for activation token. +/// +/// This could be used before running daemon processes. +/// +/// # Safety +/// +/// While the function is safe internally, it mutates the global environment +/// state for the process, hence unsafe. +pub unsafe fn set_activation_token_env(token: ActivationToken) { + env::set_var(X11_VAR, &token._token); + env::set_var(WAYLAND_VAR, token._token); +} diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index d8272161..fa3ef928 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -30,13 +30,16 @@ use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::{Event, KeyEvent}, - event_loop::{ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootELW}, + event_loop::{ + AsyncRequestSerial, ControlFlow, DeviceEvents, EventLoopClosed, + EventLoopWindowTarget as RootELW, + }, icon::Icon, keyboard::{Key, KeyCode}, platform::{modifier_supplement::KeyEventExtModifierSupplement, scancode::KeyCodeExtScancode}, window::{ - CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, - WindowAttributes, WindowButtons, WindowLevel, + ActivationToken, CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, + UserAttentionType, WindowAttributes, WindowButtons, WindowLevel, }, }; @@ -87,6 +90,7 @@ impl ApplicationName { #[derive(Clone)] pub struct PlatformSpecificWindowBuilderAttributes { pub name: Option, + pub activation_token: Option, #[cfg(x11_platform)] pub visual_infos: Option, #[cfg(x11_platform)] @@ -103,6 +107,7 @@ impl Default for PlatformSpecificWindowBuilderAttributes { fn default() -> Self { Self { name: None, + activation_token: None, #[cfg(x11_platform)] visual_infos: None, #[cfg(x11_platform)] @@ -371,6 +376,11 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.request_inner_size(size)) } + #[inline] + pub(crate) fn request_activation_token(&self) -> Result { + x11_or_wayland!(match self; Window(w) => w.request_activation_token()) + } + #[inline] pub fn set_min_inner_size(&self, dimensions: Option) { x11_or_wayland!(match self; Window(w) => w.set_min_inner_size(dimensions)) diff --git a/src/platform_impl/linux/wayland/types/xdg_activation.rs b/src/platform_impl/linux/wayland/types/xdg_activation.rs index 1befb5ff..be546d15 100644 --- a/src/platform_impl/linux/wayland/types/xdg_activation.rs +++ b/src/platform_impl/linux/wayland/types/xdg_activation.rs @@ -16,7 +16,10 @@ use sctk::reexports::protocols::xdg::activation::v1::client::xdg_activation_v1:: use sctk::globals::GlobalData; +use crate::event_loop::AsyncRequestSerial; use crate::platform_impl::wayland::state::WinitState; +use crate::platform_impl::WindowId; +use crate::window::ActivationToken; pub struct XdgActivationState { xdg_activation: XdgActivationV1, @@ -62,16 +65,29 @@ impl Dispatch for XdgA _ => return, }; - state + let global = state .xdg_activation .as_ref() .expect("got xdg_activation event without global.") - .global() - .activate(token, &data.surface); + .global(); - // Mark that no request attention is in process. - if let Some(attention_requested) = data.attention_requested.upgrade() { - attention_requested.store(false, std::sync::atomic::Ordering::Relaxed); + match data { + XdgActivationTokenData::Attention((surface, fence)) => { + global.activate(token, surface); + // Mark that no request attention is in process. + if let Some(attention_requested) = fence.upgrade() { + attention_requested.store(false, std::sync::atomic::Ordering::Relaxed); + } + } + XdgActivationTokenData::Obtain((window_id, serial)) => { + state.events_sink.push_window_event( + crate::event::WindowEvent::ActivationTokenDone { + serial: *serial, + token: ActivationToken::_new(token), + }, + *window_id, + ); + } } proxy.destroy(); @@ -79,24 +95,11 @@ impl Dispatch for XdgA } /// The data associated with the activation request. -pub struct XdgActivationTokenData { - /// The surface we're raising. - surface: WlSurface, - - /// Flag to throttle attention requests. - attention_requested: Weak, -} - -impl XdgActivationTokenData { - /// Create a new data. - /// - /// The `attenteion_requested` is marked as `false` on complition. - pub fn new(surface: WlSurface, attention_requested: Weak) -> Self { - Self { - surface, - attention_requested, - } - } +pub enum XdgActivationTokenData { + /// Request user attention for the given surface. + Attention((WlSurface, Weak)), + /// Get a token to be passed outside of the winit. + Obtain((WindowId, AsyncRequestSerial)), } delegate_dispatch!(WinitState: [ XdgActivationV1: GlobalData] => XdgActivationState); diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index 46c1b4b5..3b332aca 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -22,6 +22,7 @@ use sctk::shell::WaylandSurface; use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; use crate::event::{Ime, WindowEvent}; +use crate::event_loop::AsyncRequestSerial; use crate::platform_impl::{ Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes as PlatformAttributes, @@ -168,6 +169,14 @@ impl Window { _ => (), }; + // Activate the window when the token is passed. + if let (Some(xdg_activation), Some(token)) = ( + xdg_activation.as_ref(), + platform_attributes.activation_token, + ) { + xdg_activation.activate(token._token, &surface); + } + // XXX Do initial commit. window.commit(); @@ -496,13 +505,31 @@ impl Window { self.attention_requested.store(true, Ordering::Relaxed); let surface = self.surface().clone(); - let data = - XdgActivationTokenData::new(surface.clone(), Arc::downgrade(&self.attention_requested)); + let data = XdgActivationTokenData::Attention(( + surface.clone(), + Arc::downgrade(&self.attention_requested), + )); let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); xdg_activation_token.set_surface(&surface); xdg_activation_token.commit(); } + pub fn request_activation_token(&self) -> Result { + let xdg_activation = match self.xdg_activation.as_ref() { + Some(xdg_activation) => xdg_activation, + None => return Err(NotSupportedError::new()), + }; + + let serial = AsyncRequestSerial::get(); + + let data = XdgActivationTokenData::Obtain((self.window_id, serial)); + let xdg_activation_token = xdg_activation.get_activation_token(&self.queue_handle, data); + xdg_activation_token.set_surface(self.surface()); + xdg_activation_token.commit(); + + Ok(serial) + } + #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { self.window_state.lock().unwrap().set_cursor_grab(mode) diff --git a/src/platform_impl/linux/x11/activation.rs b/src/platform_impl/linux/x11/activation.rs new file mode 100644 index 00000000..7ae910d5 --- /dev/null +++ b/src/platform_impl/linux/x11/activation.rs @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! X11 activation handling. +//! +//! X11 has a "startup notification" specification similar to Wayland's, see this URL: +//! + +use super::{atoms::*, VoidCookie, X11Error, XConnection}; + +use std::ffi::CString; +use std::fmt::Write; + +use x11rb::protocol::xproto::{self, ConnectionExt as _}; + +impl XConnection { + /// "Request" a new activation token from the server. + pub(crate) fn request_activation_token(&self, window_title: &str) -> Result { + // The specification recommends the format "hostname+pid+"_TIME"+current time" + let uname = rustix::system::uname(); + let pid = rustix::process::getpid(); + let time = self.timestamp(); + + let activation_token = format!( + "{}{}_TIME{}", + uname.nodename().to_str().unwrap_or("winit"), + pid.as_raw_nonzero(), + time + ); + + // Set up the new startup notification. + let notification = { + let mut buffer = Vec::new(); + buffer.extend_from_slice(b"new: ID="); + quote_string(&activation_token, &mut buffer); + buffer.extend_from_slice(b" NAME="); + quote_string(window_title, &mut buffer); + buffer.extend_from_slice(b" SCREEN="); + push_display(&mut buffer, &self.default_screen_index()); + + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + self.send_message(¬ification)?; + + Ok(activation_token) + } + + /// Finish launching a window with the given startup ID. + pub(crate) fn remove_activation_token( + &self, + window: xproto::Window, + startup_id: &str, + ) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Set the _NET_STARTUP_ID property on the window. + self.xcb_connection() + .change_property( + xproto::PropMode::REPLACE, + window, + atoms[_NET_STARTUP_ID], + xproto::AtomEnum::STRING, + 8, + startup_id.len().try_into().unwrap(), + startup_id.as_bytes(), + )? + .check()?; + + // Send the message indicating that the startup is over. + let message = { + const MESSAGE_ROOT: &str = "remove: ID="; + + let mut buffer = Vec::with_capacity( + MESSAGE_ROOT + .len() + .checked_add(startup_id.len()) + .and_then(|x| x.checked_add(1)) + .unwrap(), + ); + buffer.extend_from_slice(MESSAGE_ROOT.as_bytes()); + quote_string(startup_id, &mut buffer); + CString::new(buffer) + .map_err(|err| X11Error::InvalidActivationToken(err.into_vec()))? + .into_bytes_with_nul() + }; + + self.send_message(&message) + } + + /// Send a startup notification message to the window manager. + fn send_message(&self, message: &[u8]) -> Result<(), X11Error> { + let atoms = self.atoms(); + + // Create a new window to send the message over. + let screen = self.default_root(); + let window = xproto::WindowWrapper::create_window( + self.xcb_connection(), + screen.root_depth, + screen.root, + -100, + -100, + 1, + 1, + 0, + xproto::WindowClass::INPUT_OUTPUT, + screen.root_visual, + &xproto::CreateWindowAux::new() + .override_redirect(1) + .event_mask( + xproto::EventMask::STRUCTURE_NOTIFY | xproto::EventMask::PROPERTY_CHANGE, + ), + )?; + + // Serialize the messages in 20-byte chunks. + let mut message_type = atoms[_NET_STARTUP_INFO_BEGIN]; + message + .chunks(20) + .map(|chunk| { + let mut buffer = [0u8; 20]; + buffer[..chunk.len()].copy_from_slice(chunk); + let event = + xproto::ClientMessageEvent::new(8, window.window(), message_type, buffer); + + // Set the message type to the continuation atom for the next chunk. + message_type = atoms[_NET_STARTUP_INFO]; + + event + }) + .try_for_each(|event| { + // Send each event in order. + self.xcb_connection() + .send_event( + false, + screen.root, + xproto::EventMask::PROPERTY_CHANGE, + event, + ) + .map(VoidCookie::ignore_error) + })?; + + Ok(()) + } +} + +/// Quote a literal string as per the startup notification specification. +fn quote_string(s: &str, target: &mut Vec) { + let total_len = s.len().checked_add(3).expect("quote string overflow"); + target.reserve(total_len); + + // Add the opening quote. + target.push(b'"'); + + // Iterate over the string split by literal quotes. + s.as_bytes().split(|&b| b == b'"').for_each(|part| { + // Add the part. + target.extend_from_slice(part); + + // Escape the quote. + target.push(b'\\'); + target.push(b'"'); + }); + + // Un-escape the last quote. + target.remove(target.len() - 2); +} + +/// Push a `Display` implementation to the buffer. +fn push_display(buffer: &mut Vec, display: &impl std::fmt::Display) { + struct Writer<'a> { + buffer: &'a mut Vec, + } + + impl<'a> std::fmt::Write for Writer<'a> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.buffer.extend_from_slice(s.as_bytes()); + Ok(()) + } + } + + write!(Writer { buffer }, "{}", display).unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn properly_escapes_x11_messages() { + let assert_eq = |input: &str, output: &[u8]| { + let mut buf = vec![]; + quote_string(input, &mut buf); + assert_eq!(buf, output); + }; + + assert_eq("", b"\"\""); + assert_eq("foo", b"\"foo\""); + assert_eq("foo\"bar", b"\"foo\\\"bar\""); + } +} diff --git a/src/platform_impl/linux/x11/atoms.rs b/src/platform_impl/linux/x11/atoms.rs index 662a0e61..48c7f1ed 100644 --- a/src/platform_impl/linux/x11/atoms.rs +++ b/src/platform_impl/linux/x11/atoms.rs @@ -57,6 +57,11 @@ atom_manager! { _NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_WINDOW_TYPE, + // Activation atoms. + _NET_STARTUP_INFO_BEGIN, + _NET_STARTUP_INFO, + _NET_STARTUP_ID, + // WM window types. _NET_WM_WINDOW_TYPE_DESKTOP, _NET_WM_WINDOW_TYPE_DOCK, diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index a75a3107..b049dd37 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -57,7 +57,7 @@ impl EventProcessor { } } - fn with_window(&self, window_id: xproto::Window, callback: F) -> Option + pub(crate) fn with_window(&self, window_id: xproto::Window, callback: F) -> Option where F: Fn(&Arc) -> Ret, { @@ -237,6 +237,10 @@ impl EventProcessor { // In version 0, time isn't specified x11rb::CURRENT_TIME }; + + // Log this timestamp. + wt.xconn.set_timestamp(time); + // This results in the `SelectionNotify` event below self.dnd.convert_selection(window, time); } @@ -291,6 +295,9 @@ impl EventProcessor { let window = xsel.requestor as xproto::Window; let window_id = mkwid(window); + // Set the timestamp. + wt.xconn.set_timestamp(xsel.time as xproto::Timestamp); + if xsel.property == atoms[XdndSelection] as c_ulong { let mut result = None; @@ -562,6 +569,10 @@ impl EventProcessor { // Note that in compose/pre-edit sequences, we'll always receive KeyRelease events ty @ ffi::KeyPress | ty @ ffi::KeyRelease => { let xkev: &mut ffi::XKeyEvent = xev.as_mut(); + + // Set the timestamp. + wt.xconn.set_timestamp(xkev.time as xproto::Timestamp); + let window = match self.active_window { Some(window) => window, None => return, @@ -664,6 +675,10 @@ impl EventProcessor { let xev: &ffi::XIDeviceEvent = unsafe { &*(xev.data as *const _) }; let window_id = mkwid(xev.event as xproto::Window); let device_id = mkdid(xev.deviceid); + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + if (xev.flags & ffi::XIPointerEmulated) != 0 { // Deliver multi-touch events instead of emulated mouse events. return; @@ -751,6 +766,10 @@ impl EventProcessor { } ffi::XI_Motion => { let xev: &ffi::XIDeviceEvent = unsafe { &*(xev.data as *const _) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let device_id = mkdid(xev.deviceid); let window = xev.event as xproto::Window; let window_id = mkwid(window); @@ -838,6 +857,9 @@ impl EventProcessor { ffi::XI_Enter => { let xev: &ffi::XIEnterEvent = unsafe { &*(xev.data as *const _) }; + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let window = xev.event as xproto::Window; let window_id = mkwid(window); let device_id = mkdid(xev.deviceid); @@ -881,6 +903,9 @@ impl EventProcessor { let xev: &ffi::XILeaveEvent = unsafe { &*(xev.data as *const _) }; let window = xev.event as xproto::Window; + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + // Leave, FocusIn, and FocusOut can be received by a window that's already // been destroyed, which the user presumably doesn't want to deal with. let window_closed = !self.window_exists(window); @@ -897,6 +922,9 @@ impl EventProcessor { let xev: &ffi::XIFocusInEvent = unsafe { &*(xev.data as *const _) }; let window = xev.event as xproto::Window; + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + wt.ime .borrow_mut() .focus(xev.event) @@ -958,6 +986,10 @@ impl EventProcessor { ffi::XI_FocusOut => { let xev: &ffi::XIFocusOutEvent = unsafe { &*(xev.data as *const _) }; let window = xev.event as xproto::Window; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + if !self.window_exists(window) { return; } @@ -1004,6 +1036,10 @@ impl EventProcessor { ffi::XI_TouchBegin | ffi::XI_TouchUpdate | ffi::XI_TouchEnd => { let xev: &ffi::XIDeviceEvent = unsafe { &*(xev.data as *const _) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let window = xev.event as xproto::Window; let window_id = mkwid(window); let phase = match xev.evtype { @@ -1044,6 +1080,10 @@ impl EventProcessor { ffi::XI_RawButtonPress | ffi::XI_RawButtonRelease => { let xev: &ffi::XIRawEvent = unsafe { &*(xev.data as *const _) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + if xev.flags & ffi::XIPointerEmulated == 0 { callback(Event::DeviceEvent { device_id: mkdid(xev.deviceid), @@ -1061,6 +1101,10 @@ impl EventProcessor { ffi::XI_RawMotion => { let xev: &ffi::XIRawEvent = unsafe { &*(xev.data as *const _) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let did = mkdid(xev.deviceid); let mask = unsafe { @@ -1112,6 +1156,9 @@ impl EventProcessor { ffi::XI_RawKeyPress | ffi::XI_RawKeyRelease => { let xev: &ffi::XIRawEvent = unsafe { &*(xev.data as *const _) }; + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let state = match xev.evtype { ffi::XI_RawKeyPress => Pressed, ffi::XI_RawKeyRelease => Released, @@ -1136,6 +1183,10 @@ impl EventProcessor { ffi::XI_HierarchyChanged => { let xev: &ffi::XIHierarchyEvent = unsafe { &*(xev.data as *const _) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + for info in unsafe { slice::from_raw_parts(xev.info, xev.num_info as usize) } { @@ -1168,6 +1219,10 @@ impl EventProcessor { let xev = unsafe { &*(xev as *const _ as *const ffi::XkbNewKeyboardNotifyEvent) }; + + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let keycodes_changed_flag = 0x1; let geometry_changed_flag = 0x1 << 1; @@ -1186,6 +1241,9 @@ impl EventProcessor { let xev = unsafe { &*(xev as *const _ as *const ffi::XkbStateNotifyEvent) }; + // Set the timestamp. + wt.xconn.set_timestamp(xev.time as xproto::Timestamp); + let prev_mods = self.kb_state.mods_state(); self.kb_state.update_modifiers( xev.base_mods, diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index e1d65756..fd7df53a 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -1,5 +1,6 @@ #![cfg(x11_platform)] +mod activation; mod atoms; mod dnd; mod event_processor; @@ -83,6 +84,7 @@ pub struct EventLoopWindowTarget { ime: RefCell, windows: RefCell>>, redraw_sender: Sender, + activation_sender: Sender, device_events: Cell, _marker: ::std::marker::PhantomData, } @@ -100,12 +102,17 @@ pub struct EventLoop { redraw_dispatcher: Dispatcher<'static, Channel, EventLoopState>, } +type ActivationToken = (WindowId, crate::event_loop::AsyncRequestSerial); + struct EventLoopState { /// Incoming user events. user_events: VecDeque, /// Incoming redraw events. redraw_events: VecDeque, + + /// Incoming activation tokens. + activation_tokens: VecDeque, } pub struct EventLoopProxy { @@ -261,6 +268,9 @@ impl EventLoop { // Create a channel for handling redraw requests. let (redraw_sender, redraw_channel) = channel(); + // Create a channel for sending activation tokens. + let (activation_token_sender, activation_token_channel) = channel(); + // Create a dispatcher for the redraw channel such that we can dispatch it independent of the // event loop. let redraw_dispatcher = @@ -273,6 +283,18 @@ impl EventLoop { .register_dispatcher(redraw_dispatcher.clone()) .expect("Failed to register the redraw event channel with the event loop"); + // Create a dispatcher for the activation token channel such that we can dispatch it + // independent of the event loop. + let activation_tokens = + Dispatcher::<_, EventLoopState>::new(activation_token_channel, |ev, _, state| { + if let ChanResult::Msg(token) = ev { + state.activation_tokens.push_back(token); + } + }); + handle + .register_dispatcher(activation_tokens.clone()) + .expect("Failed to register the activation token channel with the event loop"); + let kb_state = KbdState::from_x11_xkb(xconn.xcb_connection().get_raw_xcb_connection()).unwrap(); @@ -286,6 +308,7 @@ impl EventLoop { wm_delete_window, net_wm_ping, redraw_sender, + activation_sender: activation_token_sender, device_events: Default::default(), }; @@ -344,6 +367,7 @@ impl EventLoop { state: EventLoopState { user_events: VecDeque::new(), redraw_events: VecDeque::new(), + activation_tokens: VecDeque::new(), }, } } @@ -397,6 +421,34 @@ impl EventLoop { // Process all pending events this.drain_events(callback, control_flow); + // Empty activation tokens. + while let Some((window_id, serial)) = this.state.activation_tokens.pop_front() { + let token = this + .event_processor + .with_window(window_id.0 as xproto::Window, |window| { + window.generate_activation_token() + }); + + match token { + Some(Ok(token)) => sticky_exit_callback( + crate::event::Event::WindowEvent { + window_id: crate::window::WindowId(window_id), + event: crate::event::WindowEvent::ActivationTokenDone { + serial, + token: crate::window::ActivationToken::_new(token), + }, + }, + &this.target, + control_flow, + callback, + ), + Some(Err(e)) => { + log::error!("Failed to get activation token: {}", e); + } + None => {} + } + } + // Empty the user event buffer { while let Some(event) = this.state.user_events.pop_front() { @@ -753,6 +805,9 @@ pub enum X11Error { /// Got `null` from an Xlib function without a reason. UnexpectedNull(&'static str), + + /// Got an invalid activation token. + InvalidActivationToken(Vec), } impl fmt::Display for X11Error { @@ -764,6 +819,11 @@ impl fmt::Display for X11Error { X11Error::XidsExhausted(e) => write!(f, "XID range exhausted: {}", e), X11Error::X11(e) => write!(f, "X11 error: {:?}", e), X11Error::UnexpectedNull(s) => write!(f, "Xlib function returned null: {}", s), + X11Error::InvalidActivationToken(s) => write!( + f, + "Invalid activation token: {}", + std::str::from_utf8(s).unwrap_or("") + ), } } } diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index b7f6215a..75774f8d 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -23,6 +23,7 @@ use x11rb::{ use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, + event_loop::AsyncRequestSerial, platform_impl::{ x11::{atoms::*, MonitorHandle as X11MonitorHandle, X11Error}, Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, @@ -124,6 +125,7 @@ pub(crate) struct UnownedWindow { ime_sender: Mutex, pub shared_state: Mutex, redraw_sender: Sender, + activation_sender: Sender, } impl UnownedWindow { @@ -317,6 +319,7 @@ impl UnownedWindow { ime_sender: Mutex::new(event_loop.ime_sender.clone()), shared_state: SharedState::new(guessed_monitor, &window_attrs), redraw_sender: event_loop.redraw_sender.clone(), + activation_sender: event_loop.activation_sender.clone(), }; // Title must be set before mapping. Some tiling window managers (i.e. i3) use the window @@ -520,6 +523,11 @@ impl UnownedWindow { leap!(window.set_window_level_inner(window_attrs.window_level)).ignore_error(); } + // Remove the startup notification if we have one. + if let Some(startup) = pl_attribs.activation_token.as_ref() { + leap!(xconn.remove_activation_token(xwindow, &startup._token)); + } + // We never want to give the user a broken window, since by then, it's too late to handle. let window = leap!(xconn.sync_with_server().map(|_| window)); @@ -1699,6 +1707,34 @@ impl UnownedWindow { .expect_then_ignore_error("Failed to set WM hints"); } + #[inline] + pub(crate) fn generate_activation_token(&self) -> Result { + // Get the title from the WM_NAME property. + let atoms = self.xconn.atoms(); + let title = { + let title_bytes = self + .xconn + .get_property(self.xwindow, atoms[_NET_WM_NAME], atoms[UTF8_STRING]) + .expect("Failed to get title"); + + String::from_utf8(title_bytes).expect("Bad title") + }; + + // Get the activation token and then put it in the event queue. + let token = self.xconn.request_activation_token(&title)?; + + Ok(token) + } + + #[inline] + pub fn request_activation_token(&self) -> Result { + let serial = AsyncRequestSerial::get(); + self.activation_sender + .send((self.id(), serial)) + .expect("activation token channel should never be closed"); + Ok(serial) + } + #[inline] pub fn id(&self) -> WindowId { WindowId(self.xwindow as _) diff --git a/src/platform_impl/linux/x11/xdisplay.rs b/src/platform_impl/linux/x11/xdisplay.rs index 3848bdec..016f0586 100644 --- a/src/platform_impl/linux/x11/xdisplay.rs +++ b/src/platform_impl/linux/x11/xdisplay.rs @@ -2,7 +2,10 @@ use std::{ collections::HashMap, error::Error, fmt, ptr, - sync::{Arc, Mutex}, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, Mutex, + }, }; use crate::window::CursorIcon; @@ -32,6 +35,9 @@ pub(crate) struct XConnection { /// The index of the default screen. default_screen: usize, + /// The last timestamp received by this connection. + timestamp: AtomicU32, + pub latest_error: Mutex>, pub cursor_cache: Mutex, ffi::Cursor>>, } @@ -95,6 +101,7 @@ impl XConnection { xcb: Some(xcb), atoms: Box::new(atoms), default_screen, + timestamp: AtomicU32::new(0), latest_error: Mutex::new(None), cursor_cache: Default::default(), }) @@ -136,6 +143,36 @@ impl XConnection { pub fn default_root(&self) -> &xproto::Screen { &self.xcb_connection().setup().roots[self.default_screen] } + + /// Get the latest timestamp. + #[inline] + pub fn timestamp(&self) -> u32 { + self.timestamp.load(Ordering::Relaxed) + } + + /// Set the last witnessed timestamp. + #[inline] + pub fn set_timestamp(&self, timestamp: u32) { + // Store the timestamp in the slot if it's greater than the last one. + let mut last_timestamp = self.timestamp.load(Ordering::Relaxed); + loop { + let wrapping_sub = |a: xproto::Timestamp, b: xproto::Timestamp| (a as i32) - (b as i32); + + if wrapping_sub(timestamp, last_timestamp) <= 0 { + break; + } + + match self.timestamp.compare_exchange( + last_timestamp, + timestamp, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(x) => last_timestamp = x, + } + } + } } impl fmt::Debug for XConnection { diff --git a/src/window.rs b/src/window.rs index 272a454b..0353b56e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1564,3 +1564,17 @@ impl Default for ImePurpose { Self::Normal } } + +/// An opaque token used to activate the [`Window`]. +/// +/// [`Window`]: crate::window::Window +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ActivationToken { + pub(crate) _token: String, +} + +impl ActivationToken { + pub(crate) fn _new(_token: String) -> Self { + Self { _token } + } +}