As the title says. I'm writing a custom X11 window manager in Rust, using the xcb library. A specific window -- the "configuration" window for cairo-dock
-- will not take button 1 clicks when focused, despite ungrabbing button 1 on that window.
Previously, I thought that said window was not holding focus, but that turns out to not be correct. Instead, the window in question is receiving focus, but not allowing any button 1 clicks through.
Relevant code for setting focus:
#[allow(clippy::single_match)]
fn set_focus(&mut self, window: xproto::Window) {
if window != self.root && window != self.focus {
let prev = self.focus;
// ungrab focus from the previous window
xproto::ungrab_button(
self.conn,
xproto::BUTTON_INDEX_1 as u8,
self.focus,
0
);
// make sure we don't accidentally have button 1 grabbed
xproto::ungrab_button(
self.conn,
xproto::BUTTON_INDEX_1 as u8,
window,
0
);
// See https://github.com/i3/i3/blob/b61a28f156aad545d5c54b9a6f40ef7cae1a1c9b/src/x.c#L1286-L1337
if self.needs_take_focus(window)
&& self.doesnt_take_focus(window)
&& window != base::NONE
&& window != self.root {
let client_message =
xproto::ClientMessageEvent::new(
32,
window,
self.atom("WM_PROTOCOLS"),
xproto::ClientMessageData::from_data32(
[
self.atom("WM_TAKE_FOCUS"),
self.last_timestamp,
0,
0,
0
]
)
);
xproto::send_event(self.conn, false, window, base::NONE as u32, &client_message);
} else {
debug!("{} can be focused normally", window);
xproto::set_input_focus(
self.conn,
xproto::INPUT_FOCUS_PARENT as u8,
window,
self.last_timestamp,
);
}
self.replace_prop(
self.root,
self.atom("_NET_ACTIVE_WINDOW"),
32,
&[window]
);
debug!("updating _NET_WM_STATE with _NET_WM_STATE_FOCUSED!");
self.remove_prop(prev, self.atom("_NET_WM_STATE"), self.atom("_NET_WM_STATE_FOCUSED"));
self.append_prop(window, self.atom("_NET_WM_STATE"), self.atom("_NET_WM_STATE_FOCUSED"));
self.focus = window;
debug!("focused window: {}", self.focus);
} else if window == self.root {
self.remove_prop(self.focus, self.atom("_NET_WM_STATE"), self.atom("_NET_WM_STATE_FOCUSED"));
debug!("focusing root -> NONE");
self.replace_prop(self.root, self.atom("_NET_ACTIVE_WINDOW"), 32, &[base::NONE]);
xproto::set_input_focus(self.conn, 0, base::NONE, base::CURRENT_TIME);
self.focus = xcb::NONE;
}
}
fn append_prop(&self, window: xproto::Window, prop: u32, atom: u32) {
// TODO: Check result
xproto::change_property(
self.conn,
xproto::PROP_MODE_APPEND as u8,
window,
prop,
xproto::ATOM_ATOM,
32,
&[atom]
);
}
fn remove_prop(&self, window: xproto::Window, prop: u32, atom: u32) {
let cookie = xproto::get_property(self.conn, false, window, prop, xproto::GET_PROPERTY_TYPE_ANY, 0, 4096);
match cookie.get_reply() {
Ok(res) => {
match res.value::<u32>() {
[] => {},
values => {
let mut new_values: Vec<u32> = Vec::from(values);
new_values.retain(|value| value != &atom);
self.replace_prop(window, prop, 32, &new_values);
},
}
},
Err(err) => error!("couldn't get props to remove from: {:#?}", err),
}
}
fn needs_take_focus(&self, window: xproto::Window) -> bool {
let properties_cookie =
xproto::get_property(
self.conn,
false,
window,
self.atom("WM_PROTOCOLS"),
xproto::ATOM_ANY,
0,
2048
);
match properties_cookie.get_reply() {
Ok(protocols) => {
let mut needs_help = false;
for proto in protocols.value::<u32>().iter() {
match self.atom_by_id_checked(proto) {
Some("WM_TAKE_FOCUS") => {
needs_help = true
},
_ => (),
}
}
needs_help
},
// FIXME
Err(_) => false,
}
}
fn doesnt_take_focus(&self, window: xproto::Window) -> bool {
match xcb_util::icccm::get_wm_hints(self.conn, window).get_reply() {
Ok(hints) => {
if let Some(input) = hints.input() {
input
} else {
false
}
},
// FIXME
Err(_) => false,
}
}
It turns out my problem was that I wasn't ungrabbing button 1 correctly; focus was actually being passed correctly (see question edit history), I was just forgetting to ungrab correctly because I forgot that the initial grab had a button mask on it. Thank you so much to Uli Schlachter in the comments helping me get it figured out.