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
This commit is contained in:
GoranKovac 2025-12-22 01:30:50 +01:00 committed by GitHub
parent 979eab242e
commit bf738fffbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 66 additions and 18 deletions

View file

@ -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();

View file

@ -1400,11 +1400,8 @@ impl<S: X11Selection + 'static> InnerServerState<S> {
.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(

View file

@ -686,7 +686,10 @@ impl XState {
motif_hints: Option<motif::Hints>,
has_transient_for: bool,
) -> XResult<bool> {
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<Functions>,
@ -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<Decorations> 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
}
}
}

View file

@ -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]