From 4cfe2da2de5886f503ce8bf78bf15cb4f31cfb3c Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:18:00 +0200 Subject: Add global-menu support (dbusmenu/appmenu) for Wayland and X11 clients Exposes each window's application menu (Firefox/GTK's dbusmenu export, X11's _GTK_APPLICATION_OBJECT_PATH-style menus via global_menu.rs) so an external panel can render it as a system menu bar rather than each window drawing its own, the same convention appmenu.rs/gtk_shell.rs and appmenu_registrar.rs wire up across both backends. --- crates/platform/src/appmenu_registrar.rs | 142 +++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 crates/platform/src/appmenu_registrar.rs (limited to 'crates/platform') diff --git a/crates/platform/src/appmenu_registrar.rs b/crates/platform/src/appmenu_registrar.rs new file mode 100644 index 0000000..60511e6 --- /dev/null +++ b/crates/platform/src/appmenu_registrar.rs @@ -0,0 +1,142 @@ +//! `com.canonical.AppMenu.Registrar`: the classic Unity/`appmenu-qt5` +//! global-menu registration service. Unlike every other menu source srdwm +//! reads (the Wayland backend's `xwayland.rs`'s `_GTK_*`/ +//! `_UNITY_OBJECT_PATH` X11 properties and `gtk_shell.rs`'s native-Wayland +//! `gtk_surface1.set_dbus_properties`), a classic Qt app using +//! `appmenu-qt5`'s original model never puts its menu's D-Bus address on an +//! X11 property at all - it calls `RegisterWindow(windowId, menuObjectPath)` +//! on this well-known D-Bus name instead, with the *sender* of that call +//! being the only way to learn which bus name the menu actually lives on. +//! Reading properties can't discover this; something has to actually own +//! this name on the session bus and receive the call, which is what this +//! module does. +//! +//! Lives in `srdwm-platform`, not `srdwm-wayland`, even though `windowId` +//! is always a raw X11 XID (this model predates Wayland entirely): both the +//! Wayland backend's XWayland integration *and* the native X11 backend +//! (`srdwm-x11`) need the exact same service against the exact same kind of +//! id, and neither depends on the other - `srdwm-platform` is the one +//! crate both already depend on. Each backend constructs its own +//! `AppmenuRegistrarState` (only ever one should actually be running at a +//! time, since only one backend runs per session) and resolves each +//! `RegistrarEvent`'s `window_id` back to its own `WindowId` however it +//! already maps X11 ids - the Wayland backend's `xwayland.rs::apply_ +//! registrar_events` matches `X11Surface::window_id()` against `id_to_ +//! window`, the X11 backend's `xid_to_core` map directly. +//! +//! This compositor has no async runtime anywhere else (see `srdwm/src/ +//! main.rs`'s plain synchronous loop), and zbus's `blocking` API exists +//! specifically so this doesn't need one: `zbus::blocking::Connection` +//! still runs its socket-reading/dispatch on a background thread +//! internally (via `async-io`'s own global executor, which spawns that +//! thread the first time any work is submitted to it - nothing here has +//! to drive that explicitly), so holding the built `Connection` alive is +//! the only thing `AppmenuRegistrarState` needs to do to keep the service +//! running. The interface impl below only ever sends a plain event over a +//! `std::sync::mpsc` channel - draining it is a non-blocking, synchronous +//! `try_iter()` call from the compositor's own event-loop tick, the same +//! "background thread feeds a channel, the main loop drains it" shape +//! `IpcServer` already uses for the control socket. + +use std::sync::mpsc::{Receiver, Sender}; + +use zbus::message::Header; +use zbus::zvariant::OwnedObjectPath; + +/// One `RegisterWindow`/`UnregisterWindow` call, captured off the D-Bus +/// message and handed to a backend's own event-loop tick to resolve +/// against a real `WindowId` and update `Window.global_menu`. +#[derive(Debug)] +pub enum RegistrarEvent { + Registered { window_id: u32, bus_name: String, menu_path: String }, + Unregistered { window_id: u32 }, +} + +/// The D-Bus interface itself - deliberately does nothing but forward +/// each call onto `tx`. Resolving `window_id` against `WindowManager` has +/// to happen back on the compositor's own thread (everything `WindowManager` +/// touches is `Rc`/`RefCell`, not `Send`), so there is nothing more useful +/// this could do while actually running on zbus's background thread. +struct RegistrarIface { + tx: Sender, +} + +#[zbus::interface(name = "com.canonical.AppMenu.Registrar")] +impl RegistrarIface { + /// The registrar's own defined method: a window announcing (or + /// re-announcing - a client is free to call this again if its menu + /// path changes) its `com.canonical.dbusmenu` object. `#[zbus(header)]` + /// is what makes the caller's bus name visible at all - it's the + /// message envelope, not a request argument, so there is no other way + /// to learn it. + fn register_window(&mut self, window_id: u32, menu_object_path: OwnedObjectPath, #[zbus(header)] header: Header<'_>) { + let Some(bus_name) = header.sender() else { return }; + let _ = self.tx.send(RegistrarEvent::Registered { window_id, bus_name: bus_name.to_string(), menu_path: menu_object_path.to_string() }); + } + + /// The protocol's own way of saying "no menu after all" - a window + /// closing its menu, or closing entirely, without necessarily also + /// destroying the D-Bus connection that registered it. Matches + /// `xwayland.rs`/`gtk_shell.rs`'s own handling of the equivalent case: + /// clear `Window.global_menu` rather than leaving it stale. + fn unregister_window(&mut self, window_id: u32) { + let _ = self.tx.send(RegistrarEvent::Unregistered { window_id }); + } +} + +/// Owns the registrar's D-Bus connection for as long as an X11-capable +/// backend (XWayland under the Wayland backend, or the native X11 backend) +/// is running. `None` if starting the service failed (another process +/// already owns the name, or no session bus is reachable at all) - a +/// classic Qt app's global menu simply won't be seen in that case, same +/// "degrade, don't crash the compositor over a cosmetic feature" handling +/// as every other optional protocol here. +pub struct AppmenuRegistrarState { + // Never read again after construction - its only job is to outlive + // the compositor so the background-thread service it owns keeps + // running. `Option` because starting it can fail (see above). + _connection: Option, + rx: Receiver, +} + +impl AppmenuRegistrarState { + pub fn new() -> Self { + let (tx, rx) = std::sync::mpsc::channel(); + let iface = RegistrarIface { tx }; + // `replace_existing_names`: a previous srdwm process that crashed + // or was killed (rather than exiting cleanly) can leave this name + // owned until the bus itself notices the connection is gone, which + // is not guaranteed to have happened yet by the time a fresh + // process starts - same reasoning as `IpcServer::bind_in` removing + // a stale control-socket file before binding its own fresh one, so + // a restart doesn't leave the *new* process's registrar silently + // unreachable behind a name the old one still holds. + let connection = zbus::blocking::connection::Builder::session() + .and_then(|b| b.serve_at("/com/canonical/AppMenu/Registrar", iface)) + .and_then(|b| b.name("com.canonical.AppMenu.Registrar")) + .map(|b| b.replace_existing_names(true)) + .and_then(|b| b.build()); + let _connection = match connection { + Ok(c) => Some(c), + Err(e) => { + log::warn!("appmenu_registrar: couldn't start com.canonical.AppMenu.Registrar ({e}); classic Qt/appmenu-qt5 global menus won't be seen"); + None + } + }; + Self { _connection, rx } + } + + /// Every registration/unregistration that arrived since the last call, + /// non-blockingly - called once per event-loop tick from `poll_events`, + /// the same drain-a-channel shape `IpcServer::poll` already uses for + /// the control socket. + pub fn drain_events(&self) -> Vec { + self.rx.try_iter().collect() + } +} + +impl Default for AppmenuRegistrarState { + fn default() -> Self { + Self::new() + } +} -- cgit v1.2.3