srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/x11/src/platform
diff options
context:
space:
mode:
Diffstat (limited to 'crates/x11/src/platform')
-rw-r--r--crates/x11/src/platform/connect.rs1
-rw-r--r--crates/x11/src/platform/global_menu.rs90
-rw-r--r--crates/x11/src/platform/mod.rs30
-rw-r--r--crates/x11/src/platform/trait_impl.rs15
4 files changed, 134 insertions, 2 deletions
diff --git a/crates/x11/src/platform/connect.rs b/crates/x11/src/platform/connect.rs
index d5c3a59..2d2f726 100644
--- a/crates/x11/src/platform/connect.rs
+++ b/crates/x11/src/platform/connect.rs
@@ -94,6 +94,7 @@ impl X11Platform {
keyboard_mapping,
numlock_mask,
ipc,
+ appmenu_registrar: Some(srdwm_platform::AppmenuRegistrarState::new()),
})
}
diff --git a/crates/x11/src/platform/global_menu.rs b/crates/x11/src/platform/global_menu.rs
new file mode 100644
index 0000000..161fe80
--- /dev/null
+++ b/crates/x11/src/platform/global_menu.rs
@@ -0,0 +1,90 @@
+//! Global-menu support for the native X11 backend - parity with the
+//! Wayland backend's XWayland integration (`crates/wayland/src/xwayland.rs`
+//! and `srdwm_platform::appmenu_registrar`), reading the identical X11
+//! properties and running the identical `com.canonical.AppMenu.Registrar`
+//! D-Bus service, since a client-side toolkit (GTK/Qt) exports its menu the
+//! same way regardless of which X server it's actually talking to.
+
+use super::*;
+
+impl X11Platform {
+ /// Reads `xid`'s global-menu D-Bus address straight off its own X11
+ /// properties - `_GTK_UNIQUE_BUS_NAME` plus whichever menu-path atom
+ /// the client actually set. Exact mirror of `crates/wayland/src/
+ /// xwayland.rs::EwmhState::read_global_menu`; see that method's doc
+ /// comment for the full reasoning (menubar wins over app-menu, the
+ /// `appmenu-gtk-module` Unity-shim case `classify_menu_source` exists
+ /// for). No bus name means no menu at all, so this returns `None`
+ /// rather than a `GlobalMenu` with an empty `bus_name`.
+ pub(super) fn read_global_menu(&self, xid: XWindow) -> Option<srdwm_core::GlobalMenu> {
+ let read_string = |atom: u32| -> Option<String> {
+ let reply = self.conn.get_property(false, xid, atom, x11rb::protocol::xproto::AtomEnum::ANY, 0, u32::MAX).ok()?.reply().ok()?;
+ if reply.value.is_empty() {
+ return None;
+ }
+ String::from_utf8(reply.value).ok().filter(|s| !s.is_empty())
+ };
+
+ // Checked before anything GTK-atom-related - see `xwayland.rs`'s
+ // identical check in its own `read_global_menu` for why: these two
+ // are already a complete address on their own, and a Qt app under
+ // a KDE Plasma session never sets `_GTK_UNIQUE_BUS_NAME` at all.
+ if let (Some(bus_name), Some(menu_path)) = (read_string(self.atoms._KDE_NET_WM_APPMENU_SERVICE_NAME), read_string(self.atoms._KDE_NET_WM_APPMENU_OBJECT_PATH)) {
+ return Some(srdwm_core::GlobalMenu { bus_name, menu_path: Some(menu_path), app_path: None, window_path: None, source: srdwm_core::MenuSource::DbusMenu });
+ }
+
+ let bus_name = read_string(self.atoms._GTK_UNIQUE_BUS_NAME)?;
+ let app_path = read_string(self.atoms._GTK_APPLICATION_OBJECT_PATH);
+ let window_path = read_string(self.atoms._GTK_WINDOW_OBJECT_PATH);
+ let is_real_gtk_application = app_path.is_some() || window_path.is_some();
+ let gtk_menu_path = read_string(self.atoms._GTK_MENUBAR_OBJECT_PATH).or_else(|| read_string(self.atoms._GTK_APP_MENU_OBJECT_PATH));
+ let unity_path = read_string(self.atoms._UNITY_OBJECT_PATH);
+ let (menu_path, source) = srdwm_core::classify_menu_source(gtk_menu_path, is_real_gtk_application, unity_path);
+ Some(srdwm_core::GlobalMenu { bus_name, menu_path, app_path, window_path, source })
+ }
+
+ /// Refreshes the focused window's `global_menu` from its own X11
+ /// properties - call on every real focus change (`Platform::focus`,
+ /// the single chokepoint every focus path already goes through, same
+ /// role `update_net_active_window` plays for the Wayland backend).
+ /// Global-menu properties are usually set once, shortly after a client
+ /// registers on the session bus, which can race a window's own initial
+ /// map - reading only at map time would miss a client that finished
+ /// registering a moment later, so this re-reads on every focus instead.
+ /// `read_global_menu` returning `None` (the common case for anything
+ /// non-GTK, or a GTK app with no menu to export) correctly clears a
+ /// stale value left over from whichever window was focused before.
+ pub(super) fn refresh_focused_global_menu(&mut self, id: WindowId, client: XWindow) {
+ let menu = self.read_global_menu(client);
+ if let Some(w) = self.wm.borrow_mut().window_mut(id) {
+ w.global_menu = menu;
+ }
+ }
+
+ /// Drains `AppmenuRegistrarState`'s channel and applies every event to
+ /// the matching `Window.global_menu` - call once per `poll_events`
+ /// tick, same as the Wayland backend's `xwayland.rs::apply_registrar_
+ /// events`. `xid_to_core` already maps XID straight to `WindowId` here
+ /// (unlike the Wayland backend, which has to scan for the matching
+ /// `X11Surface`), so this needs no extra lookup structure of its own.
+ pub(super) fn apply_registrar_events(&mut self) {
+ let Some(registrar) = &self.appmenu_registrar else { return };
+ let events = registrar.drain_events();
+ if events.is_empty() {
+ return;
+ }
+ for event in events {
+ let (window_id, menu) = match event {
+ srdwm_platform::RegistrarEvent::Registered { window_id, bus_name, menu_path } => (
+ window_id,
+ Some(srdwm_core::GlobalMenu { bus_name, menu_path: Some(menu_path), app_path: None, window_path: None, source: srdwm_core::MenuSource::DbusMenu }),
+ ),
+ srdwm_platform::RegistrarEvent::Unregistered { window_id } => (window_id, None),
+ };
+ let Some(&id) = self.xid_to_core.get(&window_id) else { continue };
+ if let Some(w) = self.wm.borrow_mut().window_mut(id) {
+ w.global_menu = menu;
+ }
+ }
+ }
+}
diff --git a/crates/x11/src/platform/mod.rs b/crates/x11/src/platform/mod.rs
index eea936f..04c918c 100644
--- a/crates/x11/src/platform/mod.rs
+++ b/crates/x11/src/platform/mod.rs
@@ -55,6 +55,26 @@ x11rb::atom_manager! {
_NET_CLIENT_LIST,
_NET_ACTIVE_WINDOW,
UTF8_STRING,
+ // Global-menu properties - see `read_global_menu`'s doc comment.
+ // A native X11 client is exactly the same GTK/Qt app the Wayland
+ // backend's `xwayland.rs::read_global_menu` already reads these
+ // from (XWayland is just another X server as far as a toolkit is
+ // concerned), so this is the identical atom set for the identical
+ // reason.
+ _GTK_UNIQUE_BUS_NAME,
+ _GTK_APPLICATION_OBJECT_PATH,
+ _GTK_WINDOW_OBJECT_PATH,
+ _GTK_MENUBAR_OBJECT_PATH,
+ _GTK_APP_MENU_OBJECT_PATH,
+ _UNITY_OBJECT_PATH,
+ // KWin's own global-menu property pair - what `libdbusmenu-qt`'s
+ // KDE integration sets. Already a complete, unambiguous
+ // `com.canonical.dbusmenu` address on its own (no classification
+ // needed the way the GTK/Unity atoms above need), and checked
+ // first in `read_global_menu` for exactly that reason - see that
+ // method's doc comment.
+ _KDE_NET_WM_APPMENU_SERVICE_NAME,
+ _KDE_NET_WM_APPMENU_OBJECT_PATH,
}
}
@@ -121,6 +141,15 @@ pub struct X11Platform {
/// itself still starts either way, matching how the Wayland backends
/// already treat this as non-fatal.
ipc: Option<srdwm_platform::IpcServer>,
+ /// `com.canonical.AppMenu.Registrar` - the classic Qt/`appmenu-qt5`
+ /// global-menu source, see `srdwm_platform::appmenu_registrar`'s module
+ /// doc comment. Unlike the Wayland backend (where this is `None` until
+ /// XWayland finishes starting up), a native X11 session always has a
+ /// real X server the moment this struct exists, so it's started
+ /// unconditionally in `connect` - still `Option` because starting the
+ /// D-Bus service itself can independently fail (see that module's own
+ /// `None` handling).
+ appmenu_registrar: Option<srdwm_platform::AppmenuRegistrarState>,
}
@@ -139,6 +168,7 @@ impl ClonedForRender for Option<&CoreWindow> {
mod actions;
mod connect;
mod events;
+mod global_menu;
mod trait_impl;
mod window;
diff --git a/crates/x11/src/platform/trait_impl.rs b/crates/x11/src/platform/trait_impl.rs
index 0a082be..1ff7845 100644
--- a/crates/x11/src/platform/trait_impl.rs
+++ b/crates/x11/src/platform/trait_impl.rs
@@ -34,6 +34,7 @@ impl Platform for X11Platform {
out.push(e);
}
}
+ self.apply_registrar_events();
if let Some(ipc) = self.ipc.as_mut() {
if ipc.poll(&self.wm) {
out.push(Event::WorkspaceChanged);
@@ -106,9 +107,11 @@ impl Platform for X11Platform {
fn focus(&mut self, window: WindowId) -> PlatformResult<()> {
if let Some(frame) = self.frames.get(&window) {
- self.conn.set_input_focus(x11rb::protocol::xproto::InputFocus::POINTER_ROOT, frame.client, x11rb::CURRENT_TIME).map_err(err)?;
- self.conn.change_property32(x11rb::protocol::xproto::PropMode::REPLACE, self.root, self.atoms._NET_ACTIVE_WINDOW, x11rb::protocol::xproto::AtomEnum::WINDOW, &[frame.client]).map_err(err)?;
+ let client = frame.client;
+ self.conn.set_input_focus(x11rb::protocol::xproto::InputFocus::POINTER_ROOT, client, x11rb::CURRENT_TIME).map_err(err)?;
+ self.conn.change_property32(x11rb::protocol::xproto::PropMode::REPLACE, self.root, self.atoms._NET_ACTIVE_WINDOW, x11rb::protocol::xproto::AtomEnum::WINDOW, &[client]).map_err(err)?;
self.conn.flush().map_err(err)?;
+ self.refresh_focused_global_menu(window, client);
}
Ok(())
}
@@ -217,4 +220,12 @@ impl Platform for X11Platform {
self.conn.ungrab_key(0, self.root, ModMask::ANY).map_err(err)?;
Ok(())
}
+
+ fn keyboard_layout(&mut self) -> PlatformResult<String> {
+ Err(PlatformError::Unsupported("keyboard_layout"))
+ }
+
+ fn cycle_keyboard_layout(&mut self) -> PlatformResult<String> {
+ Err(PlatformError::Unsupported("cycle_keyboard_layout"))
+ }
}