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