From 5a680e4ee3bda9e2e17e287c69dafaeea38ddaf5 Mon Sep 17 00:00:00 2001 From: Ilia Malanin Date: Sat, 9 May 2026 23:06:07 +0200 Subject: [PATCH 1/2] fix(xwm): Defer X11 focus release between same-client transitions The synchronous `set_input_focus(NONE, NONE)` in `X11Surface::leave` breaks focus transitions between same-client windows. Defer the focus-out decision to the next event-loop iteration so it can be cancelled out. --- src/xwayland/xwm/mod.rs | 90 ++++++++++++++++++++++++++++++------- src/xwayland/xwm/surface.rs | 15 ++++--- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/xwayland/xwm/mod.rs b/src/xwayland/xwm/mod.rs index 0d3cf3028f3a..b7391c0e07c7 100644 --- a/src/xwayland/xwm/mod.rs +++ b/src/xwayland/xwm/mod.rs @@ -136,7 +136,7 @@ use crate::{ }, }; use atomic_float::AtomicF64; -use calloop::{Interest, LoopHandle, Mode, PostAction, generic::Generic}; +use calloop::{Interest, LoopHandle, Mode, PostAction, generic::Generic, ping}; use rustix::fs::OFlags; use std::{ cell::RefCell, @@ -147,7 +147,10 @@ use std::{ io::{AsFd, OwnedFd}, net::UnixStream, }, - sync::{Arc, Weak, atomic::Ordering}, + sync::{ + Arc, Weak, + atomic::{AtomicBool, Ordering}, + }, }; use tracing::{debug, debug_span, info, trace, warn}; use wayland_server::{DisplayHandle, Resource}; @@ -163,9 +166,9 @@ use x11rb::{ render::{ConnectionExt as _, CreatePictureAux, PictureWrapper}, xfixes::ConnectionExt as _, xproto::{ - AtomEnum, CONFIGURE_NOTIFY_EVENT, ChangeWindowAttributesAux, Colormap, ColormapAlloc, + Atom, AtomEnum, CONFIGURE_NOTIFY_EVENT, ChangeWindowAttributesAux, Colormap, ColormapAlloc, ConfigWindow, ConfigureNotifyEvent, ConfigureWindowAux, ConnectionExt, CreateGCAux, - CreateWindowAux, CursorWrapper, EventMask, FontWrapper, GcontextWrapper, ImageFormat, + CreateWindowAux, CursorWrapper, EventMask, FontWrapper, GcontextWrapper, ImageFormat, InputFocus, PixmapWrapper, PropMode, Property, QueryExtensionReply, Screen, StackMode, Visualid, WindowClass, }, }, @@ -567,6 +570,8 @@ pub struct X11Wm { is_showing_desktop: bool, + pub(super) focus_release: FocusReleaseHandle, + span: tracing::Span, } @@ -576,6 +581,64 @@ impl Drop for X11Wm { } } +#[derive(Debug, Clone)] +pub(super) struct FocusReleaseHandle { + pending: Arc, + signal: ping::Ping, + conn: Weak, + root: X11Window, + active_window: Atom, +} + +impl FocusReleaseHandle { + fn new( + conn: &Arc, + root: X11Window, + active_window: Atom, + ) -> std::io::Result<(Self, ping::PingSource)> { + let (signal, source) = ping::make_ping()?; + Ok(( + Self { + pending: Arc::new(AtomicBool::new(false)), + signal, + conn: Arc::downgrade(conn), + root, + active_window, + }, + source, + )) + } + + fn schedule(&self) { + self.pending.store(true, Ordering::Release); + self.signal.ping(); + } + + fn cancel(&self) { + self.pending.store(false, Ordering::Release); + } + + fn dispatch(&self) { + if !self.pending.swap(false, Ordering::AcqRel) { + return; + } + let Some(conn) = self.conn.upgrade() else { return }; + if let Err(err) = conn.set_input_focus(InputFocus::NONE, x11rb::NONE, x11rb::CURRENT_TIME) { + warn!("Unable to release X11 keyboard focus: {}", err); + } + if let Err(err) = conn.change_property32( + PropMode::REPLACE, + self.root, + self.active_window, + AtomEnum::WINDOW, + &[x11rb::NONE], + ) { + warn!("Unable to clear _NET_ACTIVE_WINDOW: {}", err); + } + let _ = conn.flush(); + } +} + /// Edge values for resizing /// // These values are used to indicate which edge of a surface is being dragged in a resize operation. @@ -890,6 +953,13 @@ impl X11Wm { let dnd = XWmDnd::new(&conn, &screen, &atoms)?; let wm_window = OwnedX11Window::new(win, &conn); + let (focus_release, focus_release_source) = + FocusReleaseHandle::new(&conn, screen.root, atoms._NET_ACTIVE_WINDOW)?; + { + let release = focus_release.clone(); + handle.insert_source(focus_release_source, move |_, _, _| release.dispatch())?; + } + drop(_guard); let wm = Self { id, @@ -911,6 +981,7 @@ impl X11Wm { client_list: Vec::new(), client_list_stacking: Vec::new(), is_showing_desktop: false, + focus_release, span, }; @@ -2227,17 +2298,6 @@ where )?; } } - Event::FocusOut(n) => { - if xwm.windows.iter().any(|x| x.window_id() == n.event) { - conn.change_property32( - PropMode::REPLACE, - xwm.screen.root, - xwm.atoms._NET_ACTIVE_WINDOW, - AtomEnum::WINDOW, - &[x11rb::NONE], - )?; - } - } Event::ClientMessage(msg) => { if let Some(reply) = conn.get_atom_name(msg.type_)?.reply_unchecked()? { trace!( diff --git a/src/xwayland/xwm/surface.rs b/src/xwayland/xwm/surface.rs index 10f7f7d8c480..9e66cebbbafb 100644 --- a/src/xwayland/xwm/surface.rs +++ b/src/xwayland/xwm/surface.rs @@ -58,6 +58,7 @@ pub struct X11Surface { xwm: Option, client_scale: Option>, window: X11Window, + focus_release: Option, pub(super) conn: Weak, pub(super) atoms: super::Atoms, pub(crate) state: Arc>, @@ -269,6 +270,7 @@ impl X11Surface { pending_ping_timestamp: None, })), xdnd_active, + focus_release: xwm.map(|wm| wm.focus_release.clone()), user_data: Arc::new(UserDataMap::new()), } } @@ -1375,6 +1377,10 @@ impl KeyboardTarget for X11Surface { WmInputModel::GloballyActive => (false, true), }; + if let Some(release) = &self.focus_release { + release.cancel(); + } + if let Some(conn) = self.conn.upgrade() { if set_input_focus { if let Err(err) = conn.set_input_focus(InputFocus::NONE, self.window, x11rb::CURRENT_TIME) { @@ -1417,11 +1423,10 @@ impl KeyboardTarget for X11Surface { fn leave(&self, seat: &Seat, data: &mut D, serial: Serial) { if self.input_model() == WmInputModel::None { return; - } else if let Some(conn) = self.conn.upgrade() { - if let Err(err) = conn.set_input_focus(InputFocus::NONE, x11rb::NONE, x11rb::CURRENT_TIME) { - warn!("Unable to unfocus X11Surface ({:?}): {}", self.window, err); - } - let _ = conn.flush(); + } + + if let Some(release) = &self.focus_release { + release.schedule(); } let mut state = self.state.lock().unwrap(); From eebb1ceac6cf58d9a5872e16a99b4a8792dfc2a5 Mon Sep 17 00:00:00 2001 From: Ilia Malanin Date: Tue, 12 May 2026 21:31:10 +0200 Subject: [PATCH 2/2] xwm: only clear `_NET_ACTIVE_WINDOW` when X focus is set to NONE explicitly --- src/xwayland/xwm/mod.rs | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/xwayland/xwm/mod.rs b/src/xwayland/xwm/mod.rs index b7391c0e07c7..e2120f65e6a8 100644 --- a/src/xwayland/xwm/mod.rs +++ b/src/xwayland/xwm/mod.rs @@ -166,10 +166,11 @@ use x11rb::{ render::{ConnectionExt as _, CreatePictureAux, PictureWrapper}, xfixes::ConnectionExt as _, xproto::{ - Atom, AtomEnum, CONFIGURE_NOTIFY_EVENT, ChangeWindowAttributesAux, Colormap, ColormapAlloc, + AtomEnum, CONFIGURE_NOTIFY_EVENT, ChangeWindowAttributesAux, Colormap, ColormapAlloc, ConfigWindow, ConfigureNotifyEvent, ConfigureWindowAux, ConnectionExt, CreateGCAux, CreateWindowAux, CursorWrapper, EventMask, FontWrapper, GcontextWrapper, ImageFormat, InputFocus, - PixmapWrapper, PropMode, Property, QueryExtensionReply, Screen, StackMode, Visualid, WindowClass, + NotifyDetail, PixmapWrapper, PropMode, Property, QueryExtensionReply, Screen, StackMode, + Visualid, WindowClass, }, }, rust_connection::{ConnectionError, DefaultStream, RustConnection}, @@ -586,24 +587,16 @@ pub(super) struct FocusReleaseHandle { pending: Arc, signal: ping::Ping, conn: Weak, - root: X11Window, - active_window: Atom, } impl FocusReleaseHandle { - fn new( - conn: &Arc, - root: X11Window, - active_window: Atom, - ) -> std::io::Result<(Self, ping::PingSource)> { + fn new(conn: &Arc) -> std::io::Result<(Self, ping::PingSource)> { let (signal, source) = ping::make_ping()?; Ok(( Self { pending: Arc::new(AtomicBool::new(false)), signal, conn: Arc::downgrade(conn), - root, - active_window, }, source, )) @@ -626,15 +619,6 @@ impl FocusReleaseHandle { if let Err(err) = conn.set_input_focus(InputFocus::NONE, x11rb::NONE, x11rb::CURRENT_TIME) { warn!("Unable to release X11 keyboard focus: {}", err); } - if let Err(err) = conn.change_property32( - PropMode::REPLACE, - self.root, - self.active_window, - AtomEnum::WINDOW, - &[x11rb::NONE], - ) { - warn!("Unable to clear _NET_ACTIVE_WINDOW: {}", err); - } let _ = conn.flush(); } } @@ -953,8 +937,7 @@ impl X11Wm { let dnd = XWmDnd::new(&conn, &screen, &atoms)?; let wm_window = OwnedX11Window::new(win, &conn); - let (focus_release, focus_release_source) = - FocusReleaseHandle::new(&conn, screen.root, atoms._NET_ACTIVE_WINDOW)?; + let (focus_release, focus_release_source) = FocusReleaseHandle::new(&conn)?; { let release = focus_release.clone(); handle.insert_source(focus_release_source, move |_, _, _| release.dispatch())?; @@ -2298,6 +2281,17 @@ where )?; } } + Event::FocusOut(n) if n.detail == NotifyDetail::NONE => { + if xwm.windows.iter().any(|x| x.window_id() == n.event) { + conn.change_property32( + PropMode::REPLACE, + xwm.screen.root, + xwm.atoms._NET_ACTIVE_WINDOW, + AtomEnum::WINDOW, + &[x11rb::NONE], + )?; + } + } Event::ClientMessage(msg) => { if let Some(reply) = conn.get_atom_name(msg.type_)?.reply_unchecked()? { trace!(