use std::collections::HashSet;
use std::os::fd::AsRawFd;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Arc, Mutex};
use x11rb::protocol::Event;
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, EventMask, Window};
use super::connection::{X11Connection, ignore_errors};
use super::wake_pipe::{Waker, wait_readable};
#[doc(hidden)]
pub struct WmStateWatch {
shutdown: Arc<AtomicBool>,
commands: Sender<HashSet<Window>>,
waker: Waker,
thread: Mutex<Option<std::thread::JoinHandle<()>>>,
}
impl WmStateWatch {
pub fn new(on_change: impl Fn() + Send + 'static) -> Option<Self> {
let conn = X11Connection::open().ok()?;
let waker = Waker::new().ok()?;
let wake_fd = waker.read_fd();
let shutdown = Arc::new(AtomicBool::new(false));
let (commands, received) = channel();
let thread_shutdown = shutdown.clone();
let thread = std::thread::Builder::new()
.name("teksilo-x11-wm-state".into())
.spawn(move || run(conn, &thread_shutdown, &received, wake_fd, &on_change))
.ok()?;
Some(Self {
shutdown,
commands,
waker,
thread: Mutex::new(Some(thread)),
})
}
pub fn watch_only<'a>(&self, windows: impl IntoIterator<Item = &'a winit::window::Window>) {
let ids = windows
.into_iter()
.filter_map(crate::window_system::x11_window_id)
.collect();
if self.commands.send(ids).is_ok() {
self.waker.wake();
}
}
}
impl Drop for WmStateWatch {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::SeqCst);
self.waker.wake();
if let Ok(mut slot) = self.thread.lock()
&& let Some(handle) = slot.take()
{
let _ = handle.join();
}
}
}
fn run(
conn: X11Connection,
shutdown: &AtomicBool,
commands: &Receiver<HashSet<Window>>,
wake_fd: std::os::fd::RawFd,
on_change: &dyn Fn(),
) {
let x_fd = conn.conn().stream().as_raw_fd();
let state_atom = conn.atoms().net_wm_state;
let mut watched: HashSet<Window> = HashSet::new();
while !shutdown.load(Ordering::SeqCst) {
let mut changed = false;
while let Ok(wanted) = commands.try_recv() {
for &gone in watched.difference(&wanted) {
ignore_errors(conn.conn().change_window_attributes(
gone,
&ChangeWindowAttributesAux::new().event_mask(EventMask::NO_EVENT),
));
}
for &new in wanted.difference(&watched) {
ignore_errors(conn.conn().change_window_attributes(
new,
&ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE),
));
changed = true;
}
watched = wanted;
}
loop {
match conn.poll_event() {
Ok(Some(Event::PropertyNotify(notify)))
if notify.atom == state_atom && watched.contains(¬ify.window) =>
{
changed = true;
}
Ok(Some(_)) => {}
Ok(None) => break,
Err(err) => {
eprintln!("teksilo-platform: the X11 window-state watch stopped: {err}");
return;
}
}
}
if changed {
on_change();
}
let _ = conn.flush();
wait_readable(x_fd, wake_fd, None);
}
}