From bf738fffbb26669d0ba9d7a3ecdba3b47afa8a85 Mon Sep 17 00:00:00 2001 From: GoranKovac Date: Mon, 22 Dec 2025 01:30:50 +0100 Subject: [PATCH] Detecting if UTILITY is popup (#323) Ardour uses UTILITY atom all over the place for toplevel windows: Plugins, Dialogs etc However it does not provide any MOTIF_HINTS at all. WeChat however uses them for popups and also provides MOTIF_HINTS with flags 0x2 indicating that only decorations are active. MaterialMaker also follows what WeChat is doing for right click menus. This fix assigns UTILITY as popup ONLY if MOTIF_HINTS are provided and functions are not active. Couple of apps like Godot mark their windows (_NET_WM_WINDOW_TYPE_UTILITY) with override_redirect which makes them popup by default. Potentionally is_popup can be overriden in case MOTIF functions exists with so no_function_motif would be false. This fix prefers override_redirect in case that scenario comes up. Closes #294 --- src/server/event.rs | 2 +- src/server/mod.rs | 7 ++----- src/xstate/mod.rs | 45 ++++++++++++++++++++++++++++++++------------ tests/integration.rs | 30 +++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/src/server/event.rs b/src/server/event.rs index dee6903..e4114f4 100644 --- a/src/server/event.rs +++ b/src/server/event.rs @@ -140,7 +140,7 @@ impl Event for SurfaceEvents { let needs_server_side_decorations = window_data .attrs .decorations - .is_none_or(|d| d == Decorations::Server); + .is_none_or(|d| d.is_serverside()); if mode == Mode::ServerSide || !needs_server_side_decorations { let mut role = entity.get::<&mut SurfaceRole>().unwrap(); diff --git a/src/server/mod.rs b/src/server/mod.rs index ea26452..c3cbaba 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1400,11 +1400,8 @@ impl InnerServerState { .world .get::<&client::wl_surface::WlSurface>(entity) .unwrap(); - let needs_satellite_decorations = wl_decoration.is_none() - && window - .attrs - .decorations - .is_none_or(|d| d == Decorations::Server); + let needs_satellite_decorations = + wl_decoration.is_none() && window.attrs.decorations.is_none_or(|d| d.is_serverside()); let (sat_decoration, buf) = needs_satellite_decorations .then(|| { DecorationsDataSatellite::try_new( diff --git a/src/xstate/mod.rs b/src/xstate/mod.rs index 9006335..2b76af2 100644 --- a/src/xstate/mod.rs +++ b/src/xstate/mod.rs @@ -686,7 +686,10 @@ impl XState { motif_hints: Option, has_transient_for: bool, ) -> XResult { + let mut motif_popup = false; if let Some(hints) = motif_hints { + // If MOTIF_WM_HINTS provides no decorations for client assume its a popup + motif_popup = hints.decorations.is_some_and(|d| d.is_clientside()); // If the motif hints indicate the user shouldn't be able to do anything // to the window at all, it stands to reason it's probably a popup. if hints.functions.is_some_and(|f| f.is_empty()) { @@ -735,7 +738,10 @@ impl XState { for ty in window_types { match ty { x if x == self.window_atoms.normal || x == self.window_atoms.dialog => { - is_popup = override_redirect; + is_popup = override_redirect + } + x if x == self.window_atoms.utility => { + is_popup = override_redirect || motif_popup; } x if [ self.window_atoms.menu, @@ -743,7 +749,6 @@ impl XState { self.window_atoms.dropdown_menu, self.window_atoms.tooltip, self.window_atoms.drag_n_drop, - self.window_atoms.utility, ] .contains(&x) => { @@ -1151,6 +1156,19 @@ mod motif { } } + bitflags! { + #[derive(Debug, PartialEq, Eq, Clone, Copy)] + pub struct Decorations: u32 { + const All = 1; + const Border = 2; + const Resizeh = 4; + const TitleBar = 8; + const Menu = 16; + const Minimize = 32; + const Maximize = 64; + } + } + #[derive(Default)] pub(super) struct Hints { pub(super) functions: Option, @@ -1167,25 +1185,28 @@ mod motif { ret.functions = Some(Functions::from_bits_truncate(value[1])); } if flags.contains(HintsFlags::Decorations) { - ret.decorations = value[2].try_into().ok(); + ret.decorations = Some(Decorations::from_bits_truncate(value[2])); } ret } } + impl Decorations { + pub fn is_clientside(&self) -> bool { + self.is_empty() + } - #[derive(Debug, PartialEq, Eq, Clone, Copy, num_enum::TryFromPrimitive)] - #[repr(u32)] - pub enum Decorations { - Client = 0, - Server = 1, + pub fn is_serverside(&self) -> bool { + !self.is_empty() + } } impl From for zxdg_toplevel_decoration_v1::Mode { - fn from(value: Decorations) -> Self { - match value { - Decorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide, - Decorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide, + fn from(decorations: Decorations) -> Self { + if decorations.is_empty() { + zxdg_toplevel_decoration_v1::Mode::ClientSide + } else { + zxdg_toplevel_decoration_v1::Mode::ServerSide } } } diff --git a/tests/integration.rs b/tests/integration.rs index 2125422..60ce82f 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -2048,7 +2048,37 @@ fn popup_heuristics() { connection.atoms.win_type, &[connection.atoms.win_type_utility], ); + connection.set_property( + wechat_popup, + connection.atoms.motif_wm_hints, + connection.atoms.motif_wm_hints, + &[0x2_u32, 0, 0, 0, 0], + ); f.map_as_popup(&mut connection, wechat_popup); + + let godot_popup = connection.new_window(connection.root, 10, 10, 50, 50, true); + connection.set_property( + godot_popup, + x::ATOM_ATOM, + connection.atoms.win_type, + &[connection.atoms.win_type_utility], + ); + connection.set_property( + godot_popup, + connection.atoms.motif_wm_hints, + connection.atoms.motif_wm_hints, + &[0x2_u32, 0, 0, 0, 0], + ); + f.map_as_popup(&mut connection, godot_popup); + + let ardour_toplevel = connection.new_window(connection.root, 10, 10, 50, 50, false); + connection.set_property( + ardour_toplevel, + x::ATOM_ATOM, + connection.atoms.win_type, + &[connection.atoms.win_type_utility], + ); + f.map_as_toplevel(&mut connection, ardour_toplevel); } #[test]