1use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, Ordering};
23use std::sync::{Arc, Mutex};
24use std::thread::JoinHandle;
25
26use pyo3::prelude::*;
27use pyo3::types::PyBytes;
28use x11rb::connection::Connection;
29use x11rb::protocol::xfixes::{
30 ConnectionExt as XfixesExt, CursorNotifyMask, GetCursorImageReply,
31};
32use x11rb::protocol::xproto::{
33 ClientMessageEvent, ConnectionExt as XprotoExt, CreateWindowAux, EventMask, WindowClass,
34 CLIENT_MESSAGE_EVENT,
35};
36use x11rb::protocol::Event;
37use x11rb::rust_connection::RustConnection;
38
39static CALLBACK: Mutex<Option<Py<PyAny>>> = Mutex::new(None);
41static SIZE_CAP: AtomicI32 = AtomicI32::new(32);
43static REPLAY: AtomicBool = AtomicBool::new(false);
45
46struct Monitor {
47 stop: Arc<AtomicBool>,
48 wake_win: Arc<AtomicU32>,
49 done_rx: std::sync::mpsc::Receiver<()>,
51 join: JoinHandle<()>,
52}
53
54struct Slot {
56 users: usize,
57 monitor: Option<Monitor>,
58}
59
60static SLOT: Mutex<Slot> = Mutex::new(Slot { users: 0, monitor: None });
61
62pub fn set_callback(cb: Py<PyAny>) {
64 *CALLBACK.lock().unwrap() = Some(cb);
65 let slot = SLOT.lock().unwrap();
66 if let Some(m) = slot.monitor.as_ref() {
67 REPLAY.store(true, Ordering::Release);
68 wake(&m.wake_win);
69 }
70}
71
72pub fn set_size_cap(cap: i32) {
73 SIZE_CAP.store(cap, Ordering::Relaxed);
74}
75
76pub fn acquire(size_cap: i32) {
78 SIZE_CAP.store(size_cap, Ordering::Relaxed);
79 let mut slot = SLOT.lock().unwrap();
80 slot.users += 1;
81 if slot.monitor.is_none() {
82 let stop = Arc::new(AtomicBool::new(false));
83 let wake_win = Arc::new(AtomicU32::new(0));
84 let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
85 let (tstop, twin) = (stop.clone(), wake_win.clone());
86 match std::thread::Builder::new()
87 .name("pxf-x11-cursor".into())
88 .spawn(move || {
89 let _done = done_tx;
90 monitor_thread(tstop, twin);
91 }) {
92 Ok(join) => slot.monitor = Some(Monitor { stop, wake_win, done_rx, join }),
93 Err(e) => eprintln!("[x11] cursor monitor spawn failed: {e}"),
94 }
95 }
96}
97
98pub fn release(py: Python<'_>) {
106 let monitor = {
107 let mut slot = SLOT.lock().unwrap();
108 slot.users = slot.users.saturating_sub(1);
109 if slot.users == 0 {
110 slot.monitor.take()
111 } else {
112 None
113 }
114 };
115 if let Some(m) = monitor {
116 if m.join.thread().id() == std::thread::current().id() {
117 m.stop.store(true, Ordering::SeqCst);
118 wake(&m.wake_win);
119 return;
120 }
121 py.detach(move || {
122 m.stop.store(true, Ordering::SeqCst);
123 wake(&m.wake_win);
124 match m.done_rx.recv_timeout(std::time::Duration::from_secs(2)) {
125 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
126 eprintln!("[x11] cursor monitor did not stop in time; detaching");
127 }
128 _ => {
129 let _ = m.join.join();
130 }
131 }
132 });
133 }
134}
135
136pub fn shutdown() {
140 *CALLBACK.lock().unwrap() = None;
141 let slot = SLOT.lock().unwrap();
142 if let Some(m) = slot.monitor.as_ref() {
143 m.stop.store(true, Ordering::SeqCst);
144 wake(&m.wake_win);
145 }
146}
147
148fn wake(wake_win: &AtomicU32) {
154 let win = wake_win.load(Ordering::SeqCst);
155 if win == 0 {
156 return;
157 }
158 let mut last_err = String::new();
159 for attempt in 0..2 {
160 if attempt > 0 {
161 std::thread::sleep(std::time::Duration::from_millis(100));
162 }
163 let conn = match x11rb::connect(None) {
164 Ok((c, _)) => c,
165 Err(e) => {
166 last_err = format!("connect: {e}");
167 continue;
168 }
169 };
170 let ev = ClientMessageEvent {
171 response_type: CLIENT_MESSAGE_EVENT,
172 format: 32,
173 sequence: 0,
174 window: win,
175 type_: u32::from(x11rb::protocol::xproto::AtomEnum::PRIMARY),
176 data: [0u32; 5].into(),
177 };
178 match conn
179 .send_event(false, win, EventMask::NO_EVENT, ev)
180 .map_err(|e| e.to_string())
181 .and_then(|c| c.check().map_err(|e| e.to_string()))
182 {
183 Ok(()) => return,
184 Err(e) => last_err = format!("send_event: {e}"),
185 }
186 }
187 eprintln!("[x11] cursor monitor wake failed: {last_err}");
188}
189
190type Payload = (&'static str, Vec<u8>, i32, i32);
194
195fn monitor_thread(stop: Arc<AtomicBool>, wake_win: Arc<AtomicU32>) {
196 let (conn, screen_num) = match x11rb::connect(None) {
197 Ok(v) => v,
198 Err(e) => {
199 eprintln!("[x11] cursor monitor: connect failed: {e}");
200 return;
201 }
202 };
203 let root = conn.setup().roots[screen_num].root;
204 if let Err(e) = setup(&conn, root, &wake_win) {
205 eprintln!("[x11] cursor monitor unavailable: {e}");
206 return;
207 }
208 if stop.load(Ordering::SeqCst) {
209 return;
210 }
211 let mut last: Option<Payload> = fetch_payload(&conn);
212 deliver(last.as_ref());
213 if REPLAY.swap(false, Ordering::AcqRel) {
220 if last.is_none() {
221 last = fetch_payload(&conn);
222 }
223 deliver(last.as_ref());
224 }
225 loop {
226 let event = match conn.wait_for_event() {
227 Ok(ev) => ev,
228 Err(e) => {
229 eprintln!("[x11] cursor monitor: connection lost: {e}");
230 return;
231 }
232 };
233 let mut changed = matches!(event, Event::XfixesCursorNotify(_));
234 while let Ok(Some(ev)) = conn.poll_for_event() {
236 changed |= matches!(ev, Event::XfixesCursorNotify(_));
237 }
238 if stop.load(Ordering::Relaxed) {
239 return;
240 }
241 let replay = REPLAY.swap(false, Ordering::AcqRel);
242 if changed {
243 if let Some(p) = fetch_payload(&conn) {
246 last = Some(p);
247 deliver(last.as_ref());
248 } else if replay {
249 deliver(last.as_ref());
250 }
251 } else if replay {
252 if last.is_none() {
253 last = fetch_payload(&conn);
254 }
255 deliver(last.as_ref());
256 }
257 }
258}
259
260fn setup(conn: &RustConnection, root: u32, wake_win: &AtomicU32) -> Result<(), String> {
261 conn.xfixes_query_version(5, 0)
262 .map_err(|e| format!("xfixes_query_version: {e}"))?
263 .reply()
264 .map_err(|e| format!("XFixes unavailable: {e}"))?;
265 conn.xfixes_select_cursor_input(root, CursorNotifyMask::DISPLAY_CURSOR)
266 .map_err(|e| format!("select_cursor_input: {e}"))?
267 .check()
268 .map_err(|e| format!("select_cursor_input failed: {e}"))?;
269 let win = conn
270 .generate_id()
271 .map_err(|e| format!("generate_id: {e}"))?;
272 conn.create_window(
273 0,
274 win,
275 root,
276 0,
277 0,
278 1,
279 1,
280 0,
281 WindowClass::INPUT_ONLY,
282 0,
283 &CreateWindowAux::new(),
284 )
285 .map_err(|e| format!("create_window: {e}"))?
286 .check()
287 .map_err(|e| format!("wake window: {e}"))?;
288 wake_win.store(win, Ordering::SeqCst);
289 Ok(())
290}
291
292fn fetch_payload(conn: &RustConnection) -> Option<Payload> {
296 let img = conn.xfixes_get_cursor_image().ok()?.reply().ok()?;
297 let (msg_type, png, hot_x, hot_y) = cursor_to_png(&img, SIZE_CAP.load(Ordering::Relaxed));
298 if png.is_empty() && msg_type != "hide" {
299 return None;
300 }
301 Some((msg_type, png, hot_x, hot_y))
302}
303
304fn deliver(payload: Option<&Payload>) {
307 let (msg_type, png, hot_x, hot_y) = match payload {
308 Some(p) => p,
309 None => return,
310 };
311 if crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
312 return;
313 }
314 if CALLBACK.lock().unwrap().is_none() {
315 return;
316 }
317 Python::attach(|py| {
318 let cb = {
319 CALLBACK
320 .lock()
321 .unwrap()
322 .as_ref()
323 .map(|c| c.clone_ref(py))
324 };
325 if let Some(cb) = cb {
326 let py_bytes = PyBytes::new(py, png);
327 if let Err(e) = cb.call1(py, (*msg_type, py_bytes, *hot_x, *hot_y)) {
328 e.print(py);
329 }
330 }
331 });
332}
333
334fn cursor_to_png(img: &GetCursorImageReply, cap: i32) -> (&'static str, Vec<u8>, i32, i32) {
341 let w = img.width as usize;
342 let h = img.height as usize;
343 if w == 0 || h == 0 || img.cursor_image.len() < w * h {
344 return ("hide", Vec::new(), 0, 0);
345 }
346 let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0usize, 0usize);
347 for y in 0..h {
348 for x in 0..w {
349 if img.cursor_image[y * w + x] != 0 {
350 x0 = x0.min(x);
351 y0 = y0.min(y);
352 x1 = x1.max(x);
353 y1 = y1.max(y);
354 }
355 }
356 }
357 if x0 > x1 || y0 > y1 {
358 return ("hide", Vec::new(), 0, 0);
359 }
360 let (cw, ch) = (x1 - x0 + 1, y1 - y0 + 1);
361 let mut rgba = Vec::with_capacity(cw * ch * 4);
362 for y in y0..=y1 {
363 for x in x0..=x1 {
364 let p = img.cursor_image[y * w + x];
365 rgba.extend_from_slice(&[(p >> 16) as u8, (p >> 8) as u8, p as u8, (p >> 24) as u8]);
366 }
367 }
368 let mut hot_x = img.xhot as i32 - x0 as i32;
369 let mut hot_y = img.yhot as i32 - y0 as i32;
370 let mut image = match image::RgbaImage::from_raw(cw as u32, ch as u32, rgba) {
371 Some(i) => i,
372 None => return ("error", Vec::new(), 0, 0),
373 };
374 if cap > 0 && (cw > cap as usize || ch > cap as usize) {
375 let scale = cap as f32 / cw.max(ch) as f32;
376 let nw = ((cw as f32 * scale) as u32).max(1);
377 let nh = ((ch as f32 * scale) as u32).max(1);
378 image = image::imageops::resize(&image, nw, nh, image::imageops::FilterType::Lanczos3);
379 hot_x = (hot_x as f32 * scale) as i32;
380 hot_y = (hot_y as f32 * scale) as i32;
381 }
382 crate::unpremultiply_rgba(&mut image);
383 let hot_x = hot_x.clamp(0, image.width() as i32 - 1);
388 let hot_y = hot_y.clamp(0, image.height() as i32 - 1);
389 let mut png = Vec::new();
390 match image.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) {
391 Ok(()) => ("png", png, hot_x, hot_y),
392 Err(_) => ("error", Vec::new(), 0, 0),
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 fn reply(w: u16, h: u16, xhot: u16, yhot: u16, pixels: Vec<u32>) -> GetCursorImageReply {
401 GetCursorImageReply {
402 sequence: 0,
403 length: 0,
404 x: 0,
405 y: 0,
406 width: w,
407 height: h,
408 xhot,
409 yhot,
410 cursor_serial: 1,
411 cursor_image: pixels,
412 }
413 }
414
415 #[test]
418 fn transparent_cursor_hides() {
419 let (t, data, _, _) = cursor_to_png(&reply(4, 4, 0, 0, vec![0; 16]), 32);
420 assert_eq!(t, "hide");
421 assert!(data.is_empty());
422 let (t, _, _, _) = cursor_to_png(&reply(0, 0, 0, 0, vec![]), 32);
423 assert_eq!(t, "hide");
424 }
425
426 #[test]
430 fn crop_rebases_hotspot() {
431 let mut px = vec![0u32; 16];
432 for (x, y) in [(1, 1), (2, 1), (1, 2), (2, 2)] {
433 px[y * 4 + x] = 0xFF00_0000;
434 }
435 let (t, data, hx, hy) = cursor_to_png(&reply(4, 4, 2, 2, px), 32);
436 assert_eq!(t, "png");
437 assert!(!data.is_empty());
438 assert_eq!((hx, hy), (1, 1));
439 }
440
441 #[test]
445 fn out_of_bbox_hotspot_clamped() {
446 let mut px = vec![0u32; 16];
447 let (x, y) = (2usize, 1usize);
448 px[y * 4 + x] = 0xFF00_0000;
449 let (t, _, hx, hy) = cursor_to_png(&reply(4, 4, 0, 0, px.clone()), 32);
451 assert_eq!(t, "png");
452 assert_eq!((hx, hy), (0, 0));
453 let (_, _, hx, hy) = cursor_to_png(&reply(4, 4, 3, 3, px), 32);
455 assert_eq!((hx, hy), (0, 0));
456 }
457
458 #[test]
463 fn fractional_alpha_unpremultiplied() {
464 let (t, data, _, _) = cursor_to_png(&reply(2, 2, 0, 0, vec![0x8040_2010; 4]), 32);
465 assert_eq!(t, "png");
466 let img = image::load_from_memory(&data).unwrap().to_rgba8();
467 assert_eq!(img.get_pixel(0, 0).0, [128, 64, 32, 128]);
468 let (_, data, _, _) = cursor_to_png(&reply(1, 1, 0, 0, vec![0xFF10_2030]), 32);
470 let img = image::load_from_memory(&data).unwrap().to_rgba8();
471 assert_eq!(img.get_pixel(0, 0).0, [0x10, 0x20, 0x30, 0xFF]);
472 }
473
474 #[test]
477 fn oversized_cursor_capped() {
478 let (t, data, hx, hy) = cursor_to_png(&reply(64, 64, 32, 32, vec![0xFFFF_FFFF; 64 * 64]), 16);
479 assert_eq!(t, "png");
480 let img = image::load_from_memory(&data).unwrap();
481 assert_eq!((img.width(), img.height()), (16, 16));
482 assert_eq!((hx, hy), (8, 8));
483 let (_, data, _, _) = cursor_to_png(&reply(64, 64, 32, 32, vec![0xFFFF_FFFF; 64 * 64]), 0);
485 let img = image::load_from_memory(&data).unwrap();
486 assert_eq!((img.width(), img.height()), (64, 64));
487 }
488}