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 <jtnunley01@gmail.com>
This commit is contained in:
Kirill Chibisov 2023-07-20 13:16:51 +00:00 committed by GitHub
parent 89aa7cc06e
commit f7a84a5b50
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 771 additions and 35 deletions

View file

@ -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<ApplicationName>,
pub activation_token: Option<ActivationToken>,
#[cfg(x11_platform)]
pub visual_infos: Option<XVisualInfo>,
#[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<AsyncRequestSerial, NotSupportedError> {
x11_or_wayland!(match self; Window(w) => w.request_activation_token())
}
#[inline]
pub fn set_min_inner_size(&self, dimensions: Option<Size>) {
x11_or_wayland!(match self; Window(w) => w.set_min_inner_size(dimensions))

View file

@ -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<XdgActivationTokenV1, XdgActivationTokenData, WinitState> 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<XdgActivationTokenV1, XdgActivationTokenData, WinitState> 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<AtomicBool>,
}
impl XdgActivationTokenData {
/// Create a new data.
///
/// The `attenteion_requested` is marked as `false` on complition.
pub fn new(surface: WlSurface, attention_requested: Weak<AtomicBool>) -> Self {
Self {
surface,
attention_requested,
}
}
pub enum XdgActivationTokenData {
/// Request user attention for the given surface.
Attention((WlSurface, Weak<AtomicBool>)),
/// Get a token to be passed outside of the winit.
Obtain((WindowId, AsyncRequestSerial)),
}
delegate_dispatch!(WinitState: [ XdgActivationV1: GlobalData] => XdgActivationState);

View file

@ -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<AsyncRequestSerial, NotSupportedError> {
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)

View file

@ -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:
//! <https://specifications.freedesktop.org/startup-notification-spec/startup-notification-latest.txt>
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<String, X11Error> {
// 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(&notification)?;
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<u8>) {
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<u8>, display: &impl std::fmt::Display) {
struct Writer<'a> {
buffer: &'a mut Vec<u8>,
}
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\"");
}
}

View file

@ -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,

View file

@ -57,7 +57,7 @@ impl<T: 'static> EventProcessor<T> {
}
}
fn with_window<F, Ret>(&self, window_id: xproto::Window, callback: F) -> Option<Ret>
pub(crate) fn with_window<F, Ret>(&self, window_id: xproto::Window, callback: F) -> Option<Ret>
where
F: Fn(&Arc<UnownedWindow>) -> Ret,
{
@ -237,6 +237,10 @@ impl<T: 'static> EventProcessor<T> {
// 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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
// 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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
}
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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<T: 'static> EventProcessor<T> {
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,

View file

@ -1,5 +1,6 @@
#![cfg(x11_platform)]
mod activation;
mod atoms;
mod dnd;
mod event_processor;
@ -83,6 +84,7 @@ pub struct EventLoopWindowTarget<T> {
ime: RefCell<Ime>,
windows: RefCell<HashMap<WindowId, Weak<UnownedWindow>>>,
redraw_sender: Sender<WindowId>,
activation_sender: Sender<ActivationToken>,
device_events: Cell<DeviceEvents>,
_marker: ::std::marker::PhantomData<T>,
}
@ -100,12 +102,17 @@ pub struct EventLoop<T: 'static> {
redraw_dispatcher: Dispatcher<'static, Channel<WindowId>, EventLoopState<T>>,
}
type ActivationToken = (WindowId, crate::event_loop::AsyncRequestSerial);
struct EventLoopState<T> {
/// Incoming user events.
user_events: VecDeque<T>,
/// Incoming redraw events.
redraw_events: VecDeque<WindowId>,
/// Incoming activation tokens.
activation_tokens: VecDeque<ActivationToken>,
}
pub struct EventLoopProxy<T: 'static> {
@ -261,6 +268,9 @@ impl<T: 'static> EventLoop<T> {
// 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<T: 'static> EventLoop<T> {
.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<T>>::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<T: 'static> EventLoop<T> {
wm_delete_window,
net_wm_ping,
redraw_sender,
activation_sender: activation_token_sender,
device_events: Default::default(),
};
@ -344,6 +367,7 @@ impl<T: 'static> EventLoop<T> {
state: EventLoopState {
user_events: VecDeque::new(),
redraw_events: VecDeque::new(),
activation_tokens: VecDeque::new(),
},
}
}
@ -397,6 +421,34 @@ impl<T: 'static> EventLoop<T> {
// 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<u8>),
}
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("<invalid utf8>")
),
}
}
}

View file

@ -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<ImeSender>,
pub shared_state: Mutex<SharedState>,
redraw_sender: Sender<WindowId>,
activation_sender: Sender<super::ActivationToken>,
}
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<String, X11Error> {
// 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<AsyncRequestSerial, NotSupportedError> {
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 _)

View file

@ -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<Option<XError>>,
pub cursor_cache: Mutex<HashMap<Option<CursorIcon>, 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 {