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 } + } +}