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::Event;
30use x11rb::protocol::xfixes::{ConnectionExt as XfixesExt, CursorNotifyMask, GetCursorImageReply};
31use x11rb::protocol::xproto::{
32 CLIENT_MESSAGE_EVENT, ClientMessageEvent, ConnectionExt as XprotoExt, CreateWindowAux,
33 EventMask, WindowClass,
34};
35use x11rb::rust_connection::RustConnection;
36
37static CALLBACK: Mutex<Option<Py<PyAny>>> = Mutex::new(None);
39static SIZE_CAP: AtomicI32 = AtomicI32::new(128);
42static REPLAY: AtomicBool = AtomicBool::new(false);
44
45struct Monitor {
46 stop: Arc<AtomicBool>,
47 wake_win: Arc<AtomicU32>,
48 done_rx: std::sync::mpsc::Receiver<()>,
50 join: JoinHandle<()>,
51}
52
53struct Slot {
55 users: usize,
56 monitor: Option<Monitor>,
57}
58
59static SLOT: Mutex<Slot> = Mutex::new(Slot {
60 users: 0,
61 monitor: None,
62});
63
64pub fn set_callback(cb: Option<Py<PyAny>>) {
68 let registered = cb.is_some();
69 *CALLBACK.lock().unwrap() = cb;
70 if !registered {
71 return;
72 }
73 let slot = SLOT.lock().unwrap();
74 if let Some(m) = slot.monitor.as_ref() {
75 REPLAY.store(true, Ordering::Release);
76 wake(&m.wake_win);
77 }
78}
79
80pub fn set_size_cap(cap: i32) {
81 SIZE_CAP.store(cap, Ordering::Relaxed);
82}
83
84pub fn acquire() {
86 let mut slot = SLOT.lock().unwrap();
87 slot.users += 1;
88 if slot.monitor.is_none() {
89 let stop = Arc::new(AtomicBool::new(false));
90 let wake_win = Arc::new(AtomicU32::new(0));
91 let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
92 let (tstop, twin) = (stop.clone(), wake_win.clone());
93 match std::thread::Builder::new()
94 .name("pxf-x11-cursor".into())
95 .spawn(move || {
96 let _done = done_tx;
97 monitor_thread(tstop, twin);
98 }) {
99 Ok(join) => {
100 slot.monitor = Some(Monitor {
101 stop,
102 wake_win,
103 done_rx,
104 join,
105 })
106 }
107 Err(e) => eprintln!("[X11] cursor monitor spawn failed: {e}"),
108 }
109 }
110}
111
112pub fn release(py: Python<'_>) {
120 let monitor = {
121 let mut slot = SLOT.lock().unwrap();
122 slot.users = slot.users.saturating_sub(1);
123 if slot.users == 0 {
124 slot.monitor.take()
125 } else {
126 None
127 }
128 };
129 if let Some(m) = monitor {
130 if m.join.thread().id() == std::thread::current().id() {
131 m.stop.store(true, Ordering::SeqCst);
132 wake(&m.wake_win);
133 return;
134 }
135 py.detach(move || {
136 m.stop.store(true, Ordering::SeqCst);
137 wake(&m.wake_win);
138 match m.done_rx.recv_timeout(std::time::Duration::from_secs(2)) {
139 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
140 eprintln!("[X11] cursor monitor did not stop in time; detaching");
141 }
142 _ => {
143 let _ = m.join.join();
144 }
145 }
146 });
147 }
148}
149
150pub fn shutdown() {
154 *CALLBACK.lock().unwrap() = None;
155 let slot = SLOT.lock().unwrap();
156 if let Some(m) = slot.monitor.as_ref() {
157 m.stop.store(true, Ordering::SeqCst);
158 wake(&m.wake_win);
159 }
160}
161
162fn wake(wake_win: &AtomicU32) {
168 let win = wake_win.load(Ordering::SeqCst);
169 if win == 0 {
170 return;
171 }
172 let mut last_err = String::new();
173 for attempt in 0..2 {
174 if attempt > 0 {
175 std::thread::sleep(std::time::Duration::from_millis(100));
176 }
177 let conn = match x11rb::connect(None) {
178 Ok((c, _)) => c,
179 Err(e) => {
180 last_err = format!("connect: {e}");
181 continue;
182 }
183 };
184 let ev = ClientMessageEvent {
185 response_type: CLIENT_MESSAGE_EVENT,
186 format: 32,
187 sequence: 0,
188 window: win,
189 type_: u32::from(x11rb::protocol::xproto::AtomEnum::PRIMARY),
190 data: [0u32; 5].into(),
191 };
192 match conn
193 .send_event(false, win, EventMask::NO_EVENT, ev)
194 .map_err(|e| e.to_string())
195 .and_then(|c| c.check().map_err(|e| e.to_string()))
196 {
197 Ok(()) => return,
198 Err(e) => last_err = format!("send_event: {e}"),
199 }
200 }
201 eprintln!("[X11] cursor monitor wake failed: {last_err}");
202}
203
204type Payload = (&'static str, Vec<u8>, i32, i32);
208
209fn monitor_thread(stop: Arc<AtomicBool>, wake_win: Arc<AtomicU32>) {
210 let (conn, screen_num) = match x11rb::connect(None) {
211 Ok(v) => v,
212 Err(e) => {
213 eprintln!("[X11] cursor monitor: connect failed: {e}");
214 return;
215 }
216 };
217 let root = conn.setup().roots[screen_num].root;
218 if let Err(e) = setup(&conn, root, &wake_win) {
219 eprintln!("[X11] cursor monitor unavailable: {e}");
220 return;
221 }
222 if stop.load(Ordering::SeqCst) {
223 return;
224 }
225 let mut last: Option<Payload> = fetch_payload(&conn);
226 deliver(last.as_ref());
227 if REPLAY.swap(false, Ordering::AcqRel) {
234 if last.is_none() {
235 last = fetch_payload(&conn);
236 }
237 deliver(last.as_ref());
238 }
239 loop {
240 let event = match conn.wait_for_event() {
241 Ok(ev) => ev,
242 Err(e) => {
243 eprintln!("[X11] cursor monitor: connection lost: {e}");
244 return;
245 }
246 };
247 let mut changed = matches!(event, Event::XfixesCursorNotify(_));
248 while let Ok(Some(ev)) = conn.poll_for_event() {
250 changed |= matches!(ev, Event::XfixesCursorNotify(_));
251 }
252 if stop.load(Ordering::Relaxed) {
253 return;
254 }
255 let replay = REPLAY.swap(false, Ordering::AcqRel);
256 if changed {
257 if let Some(p) = fetch_payload(&conn) {
260 last = Some(p);
261 deliver(last.as_ref());
262 } else if replay {
263 deliver(last.as_ref());
264 }
265 } else if replay {
266 if last.is_none() {
267 last = fetch_payload(&conn);
268 }
269 deliver(last.as_ref());
270 }
271 }
272}
273
274fn setup(conn: &RustConnection, root: u32, wake_win: &AtomicU32) -> Result<(), String> {
275 conn.xfixes_query_version(5, 0)
276 .map_err(|e| format!("xfixes_query_version: {e}"))?
277 .reply()
278 .map_err(|e| format!("XFixes unavailable: {e}"))?;
279 conn.xfixes_select_cursor_input(root, CursorNotifyMask::DISPLAY_CURSOR)
280 .map_err(|e| format!("select_cursor_input: {e}"))?
281 .check()
282 .map_err(|e| format!("select_cursor_input failed: {e}"))?;
283 let win = conn
284 .generate_id()
285 .map_err(|e| format!("generate_id: {e}"))?;
286 conn.create_window(
287 0,
288 win,
289 root,
290 0,
291 0,
292 1,
293 1,
294 0,
295 WindowClass::INPUT_ONLY,
296 0,
297 &CreateWindowAux::new(),
298 )
299 .map_err(|e| format!("create_window: {e}"))?
300 .check()
301 .map_err(|e| format!("wake window: {e}"))?;
302 wake_win.store(win, Ordering::SeqCst);
303 Ok(())
304}
305
306fn fetch_payload(conn: &RustConnection) -> Option<Payload> {
310 let img = conn.xfixes_get_cursor_image().ok()?.reply().ok()?;
311 let (msg_type, png, hot_x, hot_y) = cursor_to_png(&img, SIZE_CAP.load(Ordering::Relaxed));
312 if png.is_empty() && msg_type != "hide" {
313 return None;
314 }
315 Some((msg_type, png, hot_x, hot_y))
316}
317
318fn deliver(payload: Option<&Payload>) {
321 let (msg_type, png, hot_x, hot_y) = match payload {
322 Some(p) => p,
323 None => return,
324 };
325 if crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
326 return;
327 }
328 if CALLBACK.lock().unwrap().is_none() {
329 return;
330 }
331 Python::attach(|py| {
332 let cb = { CALLBACK.lock().unwrap().as_ref().map(|c| c.clone_ref(py)) };
333 if let Some(cb) = cb {
334 let py_bytes = PyBytes::new(py, png);
335 if let Err(e) = cb.call1(py, (*msg_type, py_bytes, *hot_x, *hot_y)) {
336 e.print(py);
337 }
338 }
339 });
340}
341
342fn cursor_to_png(img: &GetCursorImageReply, cap: i32) -> (&'static str, Vec<u8>, i32, i32) {
349 let w = img.width as usize;
350 let h = img.height as usize;
351 if w == 0 || h == 0 || img.cursor_image.len() < w * h {
352 return ("hide", Vec::new(), 0, 0);
353 }
354 let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0usize, 0usize);
355 for y in 0..h {
356 for x in 0..w {
357 if img.cursor_image[y * w + x] != 0 {
358 x0 = x0.min(x);
359 y0 = y0.min(y);
360 x1 = x1.max(x);
361 y1 = y1.max(y);
362 }
363 }
364 }
365 if x0 > x1 || y0 > y1 {
366 return ("hide", Vec::new(), 0, 0);
367 }
368 let (cw, ch) = (x1 - x0 + 1, y1 - y0 + 1);
369 let mut rgba = Vec::with_capacity(cw * ch * 4);
370 for y in y0..=y1 {
371 for x in x0..=x1 {
372 let p = img.cursor_image[y * w + x];
373 rgba.extend_from_slice(&[(p >> 16) as u8, (p >> 8) as u8, p as u8, (p >> 24) as u8]);
374 }
375 }
376 let mut hot_x = img.xhot as i32 - x0 as i32;
377 let mut hot_y = img.yhot as i32 - y0 as i32;
378 let mut image = match image::RgbaImage::from_raw(cw as u32, ch as u32, rgba) {
379 Some(i) => i,
380 None => return ("error", Vec::new(), 0, 0),
381 };
382 if cap > 0 && (cw > cap as usize || ch > cap as usize) {
383 let scale = cap as f32 / cw.max(ch) as f32;
384 let nw = ((cw as f32 * scale) as u32).max(1);
385 let nh = ((ch as f32 * scale) as u32).max(1);
386 image = image::imageops::resize(&image, nw, nh, image::imageops::FilterType::Lanczos3);
387 hot_x = (hot_x as f32 * scale) as i32;
388 hot_y = (hot_y as f32 * scale) as i32;
389 }
390 crate::unpremultiply_rgba(&mut image);
391 let hot_x = hot_x.clamp(0, image.width() as i32 - 1);
396 let hot_y = hot_y.clamp(0, image.height() as i32 - 1);
397 let mut png = Vec::new();
398 match image.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) {
399 Ok(()) => ("png", png, hot_x, hot_y),
400 Err(_) => ("error", Vec::new(), 0, 0),
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn reply(w: u16, h: u16, xhot: u16, yhot: u16, pixels: Vec<u32>) -> GetCursorImageReply {
409 GetCursorImageReply {
410 sequence: 0,
411 length: 0,
412 x: 0,
413 y: 0,
414 width: w,
415 height: h,
416 xhot,
417 yhot,
418 cursor_serial: 1,
419 cursor_image: pixels,
420 }
421 }
422
423 #[test]
426 fn transparent_cursor_hides() {
427 let (t, data, _, _) = cursor_to_png(&reply(4, 4, 0, 0, vec![0; 16]), 32);
428 assert_eq!(t, "hide");
429 assert!(data.is_empty());
430 let (t, _, _, _) = cursor_to_png(&reply(0, 0, 0, 0, vec![]), 32);
431 assert_eq!(t, "hide");
432 }
433
434 #[test]
438 fn application_sprite_travels_whole_within_the_cap() {
439 let opaque = vec![0xff00_0000u32; 128 * 128];
441 let (t, data, _, _) = cursor_to_png(&reply(128, 128, 0, 0, opaque.clone()), 128);
442 assert_eq!(t, "png");
443 let img = image::load_from_memory(&data).unwrap();
444 assert_eq!((img.width(), img.height()), (128, 128));
445 let big = vec![0xff00_0000u32; 256 * 256];
447 let (_, data, _, _) = cursor_to_png(&reply(256, 256, 0, 0, big), 128);
448 let img = image::load_from_memory(&data).unwrap();
449 assert_eq!((img.width(), img.height()), (128, 128));
450 }
451
452 #[test]
453 fn crop_rebases_hotspot() {
454 let mut px = vec![0u32; 16];
455 for (x, y) in [(1, 1), (2, 1), (1, 2), (2, 2)] {
456 px[y * 4 + x] = 0xFF00_0000;
457 }
458 let (t, data, hx, hy) = cursor_to_png(&reply(4, 4, 2, 2, px), 32);
459 assert_eq!(t, "png");
460 assert!(!data.is_empty());
461 assert_eq!((hx, hy), (1, 1));
462 }
463
464 #[test]
468 fn out_of_bbox_hotspot_clamped() {
469 let mut px = vec![0u32; 16];
470 let (x, y) = (2usize, 1usize);
471 px[y * 4 + x] = 0xFF00_0000;
472 let (t, _, hx, hy) = cursor_to_png(&reply(4, 4, 0, 0, px.clone()), 32);
474 assert_eq!(t, "png");
475 assert_eq!((hx, hy), (0, 0));
476 let (_, _, hx, hy) = cursor_to_png(&reply(4, 4, 3, 3, px), 32);
478 assert_eq!((hx, hy), (0, 0));
479 }
480
481 #[test]
486 fn fractional_alpha_unpremultiplied() {
487 let (t, data, _, _) = cursor_to_png(&reply(2, 2, 0, 0, vec![0x8040_2010; 4]), 32);
488 assert_eq!(t, "png");
489 let img = image::load_from_memory(&data).unwrap().to_rgba8();
490 assert_eq!(img.get_pixel(0, 0).0, [128, 64, 32, 128]);
491 let (_, data, _, _) = cursor_to_png(&reply(1, 1, 0, 0, vec![0xFF10_2030]), 32);
493 let img = image::load_from_memory(&data).unwrap().to_rgba8();
494 assert_eq!(img.get_pixel(0, 0).0, [0x10, 0x20, 0x30, 0xFF]);
495 }
496
497 #[test]
500 fn oversized_cursor_capped() {
501 let (t, data, hx, hy) =
502 cursor_to_png(&reply(64, 64, 32, 32, vec![0xFFFF_FFFF; 64 * 64]), 16);
503 assert_eq!(t, "png");
504 let img = image::load_from_memory(&data).unwrap();
505 assert_eq!((img.width(), img.height()), (16, 16));
506 assert_eq!((hx, hy), (8, 8));
507 let (_, data, _, _) = cursor_to_png(&reply(64, 64, 32, 32, vec![0xFFFF_FFFF; 64 * 64]), 0);
509 let img = image::load_from_memory(&data).unwrap();
510 assert_eq!((img.width(), img.height()), (64, 64));
511 }
512}