srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-10-31 00:18:00 +0200
committersrdusr <[email protected]>2024-10-31 00:18:00 +0200
commit4cfe2da2de5886f503ce8bf78bf15cb4f31cfb3c (patch)
tree30281a9b7f24e1c996450289738a2a71b2aefbec /crates/core
parent9748bca006cd2498c4c0f2730d8789c667bda52b (diff)
downloadsrdwm-4cfe2da2de5886f503ce8bf78bf15cb4f31cfb3c.tar.gz
srdwm-4cfe2da2de5886f503ce8bf78bf15cb4f31cfb3c.zip
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.
Diffstat (limited to 'crates/core')
-rw-r--r--crates/core/src/window.rs111
1 files changed, 106 insertions, 5 deletions
diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs
index 850b936..7df7b5c 100644
--- a/crates/core/src/window.rs
+++ b/crates/core/src/window.rs
@@ -45,16 +45,78 @@ pub struct GlobalMenu {
/// `app.xxx`/`win.xxx`; a consumer must insert two action groups, under
/// prefixes `"app"` and `"win"`, from [`GlobalMenu::app_path`]/
/// [`GlobalMenu::window_path`] respectively.
-/// - [`MenuSource::Unity`]: the older Ubuntu Unity-era export
-/// (`_UNITY_OBJECT_PATH`, still relevant for some Qt platform-theme
-/// builds). Items reference actions as `unity.xxx`, all against one
-/// group at the menu's own path - a consumer inserts a single group
-/// under prefix `"unity"` instead.
+/// - [`MenuSource::Unity`]: still a `GMenuModel` underneath - same
+/// `org.gtk.Menus`/`Gio.DBusMenuModel`-compatible wire content as
+/// [`MenuSource::Gtk`] - but from `appmenu-gtk-module`'s Unity-
+/// compatibility shim (a plain `Gtk.Window` with no `GtkApplication`),
+/// which serves it under one `unity.xxx`-prefixed action group at the
+/// menu's own path instead of separate `app.xxx`/`win.xxx` groups.
+/// Confirmed live by an AGS peer session reading the actual bus content
+/// for this exact case: `_GTK_MENUBAR_OBJECT_PATH` and
+/// `_UNITY_OBJECT_PATH` set to the *same* `org.gtk.Menus` object, real
+/// content, `unity.File`/`unity.Edit` actions - not a different wire
+/// protocol, just a different action-group prefix. A consumer that
+/// already speaks `Gio.DBusMenuModel` for [`Self::Gtk`] needs nothing
+/// more than reading this variant to also handle this one.
+/// - [`MenuSource::DbusMenu`]: a genuinely different wire protocol,
+/// `com.canonical.dbusmenu` - what a client sets `_UNITY_OBJECT_PATH`
+/// for *without* any `_GTK_*` atom alongside it (the original pre-GTK3.4
+/// Ubuntu Unity export this atom was created for, and what `appmenu-
+/// qt5`'s classic Unity-registrar model still uses), or what the
+/// Wayland-native `org_kde_kwin_appmenu` protocol always carries.
+/// `Gio.DBusMenuModel` cannot read this at all - pointed at a
+/// `com.canonical.dbusmenu` object it silently returns an empty model,
+/// the same class of silent failure as every other menu-source bug this
+/// session - a consumer needs an actual dbusmenu client for this one,
+/// not a differently-prefixed `GMenuModel` read.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum MenuSource {
#[default]
Gtk,
Unity,
+ DbusMenu,
+}
+
+/// The decision every X11-property-reading global-menu source needs --
+/// pulled out as a pure function, shared by the Wayland backend's
+/// `xwayland.rs::read_global_menu` and the native X11 backend, so it's
+/// unit-testable without a real X connection and can't drift into two
+/// differently-behaving copies.
+///
+/// `gtk_menu_path` present (real `GtkApplication` or not) always means
+/// `org.gtk.Menus` - that's the only thing `_GTK_MENUBAR_OBJECT_PATH`/
+/// `_GTK_APP_MENU_OBJECT_PATH` ever address, GTK-module shim included (see
+/// [`MenuSource::Unity`]'s own doc comment for the live evidence). Only a
+/// bare `unity_path`, with no GTK atom at all, gets [`MenuSource::DbusMenu`]:
+/// that's `_UNITY_OBJECT_PATH` doing the job it was actually created for --
+/// the original pre-GTK3.4 Unity/`libdbusmenu` export, and what a non-GTK
+/// client (`appmenu-qt5`'s classic model) still uses it for - rather than
+/// `appmenu-gtk-module` setting it as a compatibility alias alongside a GTK
+/// atom that already answers the question on its own.
+///
+/// `is_real_gtk_application` overrides "a GTK path exists" rather than the
+/// reverse: a real `GMenuModel` export (`app.`/`win.`-prefixed actions)
+/// only ever comes from a `GtkApplication`, which always also sets
+/// `_GTK_APPLICATION_OBJECT_PATH`/`_GTK_WINDOW_OBJECT_PATH` (or their
+/// `gtk_shell1` equivalents) - if both are absent despite a GTK menubar
+/// path existing, `appmenu-gtk-module` is exporting a plain window's menu
+/// through its Unity-compatibility shim instead: real content, at this
+/// same path, but under `unity.`-prefixed actions. Getting this wrong means
+/// every menu item renders permanently insensitive against action groups
+/// the app never inserted - a silent failure that reads exactly like a
+/// broken app, not a wiring bug. Confirmed live by an AGS peer session
+/// reading the actual exported menu content off the bus for exactly this
+/// case (`_GTK_MENUBAR_OBJECT_PATH` set, `app_path`/`window_path` both
+/// empty, every action `unity.*`).
+pub fn classify_menu_source(gtk_menu_path: Option<String>, is_real_gtk_application: bool, unity_path: Option<String>) -> (Option<String>, MenuSource) {
+ match gtk_menu_path {
+ Some(path) if !is_real_gtk_application => (Some(path), MenuSource::Unity),
+ Some(path) => (Some(path), MenuSource::Gtk),
+ None => match unity_path {
+ Some(path) => (Some(path), MenuSource::DbusMenu),
+ None => (None, MenuSource::Gtk),
+ },
+ }
}
/// State of a single managed window. This is platform-independent: backends
@@ -90,6 +152,11 @@ pub struct Window {
pub always_on_top: bool,
pub border_color: (u8, u8, u8),
pub border_width: u32,
+ /// Titlebar/border-strip corner radius, in logical pixels. Copied from
+ /// `ThemeConfig::default_corner_radius` at creation (see `WindowManager
+ /// ::add_window`), same as `border_color`/`border_width`; a rule's own
+ /// `corner_radius` action still wins afterward.
+ pub corner_radius: u32,
/// This window's own content opacity, `0.0`..=`1.0`. Only the content
/// (the client's own surface tree) is affected - srdwm's own
/// decoration (titlebar/border/shadow) always renders fully opaque
@@ -147,6 +214,7 @@ impl Window {
always_on_top: false,
border_color: (136, 192, 208), // Nord accent, matches legacy theme default
border_width: 2,
+ corner_radius: 6,
opacity: 1.0,
resize_margin: None,
workspace: 0,
@@ -396,6 +464,39 @@ mod tests {
}
#[test]
+ fn appmenu_gtk_module_shim_is_classified_as_unity_not_gtk() {
+ // The exact live case that motivated this: a GTK menubar path
+ // present, but no application/window object path - confirmed by
+ // an AGS peer session reading the actual exported menu content off
+ // the bus and finding `unity.`-prefixed actions despite the GTK
+ // atom being what resolved the path.
+ let (path, source) = classify_menu_source(Some("/org/appmenu/gtk/window/0".to_string()), false, None);
+ assert_eq!(path.as_deref(), Some("/org/appmenu/gtk/window/0"), "the path itself is still correct - only the label was wrong");
+ assert_eq!(source, MenuSource::Unity);
+ }
+
+ #[test]
+ fn real_gtk_application_export_is_still_classified_as_gtk() {
+ let (path, source) = classify_menu_source(Some("/org/gtk/menus/window/1".to_string()), true, None);
+ assert_eq!(path.as_deref(), Some("/org/gtk/menus/window/1"));
+ assert_eq!(source, MenuSource::Gtk);
+ }
+
+ #[test]
+ fn plain_unity_object_path_with_no_gtk_atom_is_real_dbusmenu() {
+ let (path, source) = classify_menu_source(None, false, Some("/com/canonical/menu/1".to_string()));
+ assert_eq!(path.as_deref(), Some("/com/canonical/menu/1"));
+ assert_eq!(source, MenuSource::DbusMenu);
+ }
+
+ #[test]
+ fn neither_path_present_is_none() {
+ let (path, source) = classify_menu_source(None, false, None);
+ assert_eq!(path, None);
+ assert_eq!(source, MenuSource::Gtk);
+ }
+
+ #[test]
fn close_button_is_top_right_corner_of_titlebar() {
let f = frame();
let hit = ResizeEdge::hit_test(f, f.right() - 5, f.y + 5, true, 0, RESIZE_MARGIN);