diff --git a/CHANGELOG.md b/CHANGELOG.md index e96831c38e2..b640c99d3a5 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. - **Breaking:** Rename `Window::set_inner_size` to `Window::request_inner_size` and indicate if the size was applied immediately. - On X11, fix false positive flagging of key repeats when pressing different keys with no release between presses. - Implement `PartialOrd` and `Ord` for `KeyCode` and `NativeKeyCode`. diff --git a/src/event.rs b/src/event.rs index e419110a949..eccc347fb1b 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 f8e373ed753..1485069d0cf 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 1985b7d7d6c..a0d1c45d02e 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -31,6 +31,8 @@ pub mod web; pub mod windows; #[cfg(x11_platform)] pub mod x11; +#[cfg(any(x11_platform, wayland_platform))] +pub mod startup_notify; pub mod modifier_supplement; #[cfg(any( diff --git a/src/platform/startup_notify.rs b/src/platform/startup_notify.rs new file mode 100644 index 00000000000..323c2fe6244 --- /dev/null +++ b/src/platform/startup_notify.rs @@ -0,0 +1,99 @@ +//! 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. +pub 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. +pub 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 d82721616f1..fa3ef92843d 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/event_loop/mod.rs b/src/platform_impl/linux/wayland/event_loop/mod.rs index 6ddb457115d..c05954e3218 100644 --- a/src/platform_impl/linux/wayland/event_loop/mod.rs +++ b/src/platform_impl/linux/wayland/event_loop/mod.rs @@ -17,9 +17,11 @@ use sctk::reexports::client::globals; use sctk::reexports::client::{Connection, Proxy, QueueHandle, WaylandSource}; use crate::dpi::{LogicalSize, PhysicalSize}; + use crate::event::{Event, StartCause, WindowEvent}; use crate::event_loop::{ControlFlow, EventLoopWindowTarget as RootEventLoopWindowTarget}; use crate::platform_impl::platform::sticky_exit_callback; + use crate::platform_impl::EventLoopWindowTarget as PlatformEventLoopWindowTarget; mod proxy; @@ -29,6 +31,7 @@ pub use proxy::EventLoopProxy; use sink::EventSink; use super::state::{WindowCompositorUpdate, WinitState}; + use super::{DeviceId, WindowId}; type WaylandDispatcher = calloop::Dispatcher<'static, WaylandSource, WinitState>; diff --git a/src/platform_impl/linux/wayland/types/xdg_activation.rs b/src/platform_impl/linux/wayland/types/xdg_activation.rs index 1befb5ff0ac..be546d15bff 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); - - // 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); + .global(); + + 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 46c1b4b5876..3b332aca997 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/mod.rs b/src/platform_impl/linux/x11/mod.rs index e1d6575669c..cf089f27e64 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -62,7 +62,7 @@ use self::{ }; use super::common::xkb_state::KbdState; use crate::{ - error::OsError as RootOsError, + error::{OsError as RootOsError}, event::{Event, StartCause}, event_loop::{ControlFlow, DeviceEvents, EventLoopClosed, EventLoopWindowTarget as RootELW}, platform_impl::{ diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index b7f6215a614..8ff42630367 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, @@ -1699,6 +1700,11 @@ impl UnownedWindow { .expect_then_ignore_error("Failed to set WM hints"); } + #[inline] + pub fn request_activation_token(&self) -> Result { + Err(NotSupportedError::new()) + } + #[inline] pub fn id(&self) -> WindowId { WindowId(self.xwindow as _) diff --git a/src/window.rs b/src/window.rs index 272a454b919..0353b56e0bb 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 } + } +}