1use image::{ImageBuffer, Rgba, RgbaImage};
12use pyo3::prelude::*;
13use pyo3::types::PyBytes;
14use std::collections::HashMap;
15use std::io::Cursor as IoCursor;
16use std::io::Read;
17use std::time::Duration;
18use xcursor::{
19 CursorTheme,
20 parser::{Image, parse_xcursor},
21};
22
23pub enum CursorJob {
29 SetCallback(Option<Py<PyAny>>),
31 SetSize(i32),
33 SetSizeCap(i32),
36 Named {
37 name: &'static str,
38 },
39 Hide,
40 Shm {
42 hash: u64,
43 width: i32,
44 height: i32,
45 stride: i32,
46 offset: i32,
47 opaque: bool,
48 bytes: Vec<u8>,
49 hot_x: i32,
50 hot_y: i32,
51 },
52 Gles {
54 hash: u64,
55 width: i32,
56 height: i32,
57 bytes: Vec<u8>,
58 hot_x: i32,
59 hot_y: i32,
60 },
61}
62
63pub fn spawn_cursor_worker(cursor_size: i32, size_cap: i32) -> std::sync::mpsc::Sender<CursorJob> {
67 let (tx, rx) = std::sync::mpsc::channel::<CursorJob>();
68 let _ = std::thread::Builder::new()
69 .name("wl-cursor".into())
70 .spawn(move || {
71 let mut helper = Cursor::load(cursor_size);
72 let mut cap = size_cap;
73 let mut cache: HashMap<(u64, i32), CappedSprite> = HashMap::new();
74 let mut callback: Option<Py<PyAny>> = None;
75 while let Ok(job) = rx.recv() {
76 let (msg_type, data, hot_x, hot_y): (&str, Vec<u8>, i32, i32) = match job {
77 CursorJob::SetCallback(cb) => {
78 Python::attach(|_| callback = cb);
82 continue;
83 }
84 CursorJob::SetSize(size) => {
85 helper = Cursor::load(size);
86 continue;
87 }
88 CursorJob::SetSizeCap(c) => {
89 cap = c;
90 continue;
91 }
92 CursorJob::Named { name } => match helper.get_sprite(name) {
93 Some((img, x, y)) => match cap_and_encode(img, cap) {
94 Some(sprite) => {
95 let (sx, sy) = scaled_hotspot(&sprite, x as i32, y as i32);
96 ("png", sprite.png, sx, sy)
97 }
98 None => ("error", Vec::new(), 0, 0),
99 },
100 None => ("error", Vec::new(), 0, 0),
101 },
102 CursorJob::Hide => ("hide", Vec::new(), 0, 0),
103 CursorJob::Shm {
104 hash,
105 width,
106 height,
107 stride,
108 offset,
109 opaque,
110 bytes,
111 hot_x,
112 hot_y,
113 } => capped_job(&mut cache, hash, cap, hot_x, hot_y, || {
114 decode_shm_cursor(width, height, stride, offset, opaque, &bytes)
115 }),
116 CursorJob::Gles {
117 hash,
118 width,
119 height,
120 bytes,
121 hot_x,
122 hot_y,
123 } => capped_job(&mut cache, hash, cap, hot_x, hot_y, || {
124 decode_gles_cursor(width, height, &bytes)
125 }),
126 };
127 if data.is_empty() && msg_type != "hide" {
131 continue;
132 }
133 if crate::PY_SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) {
134 continue;
135 }
136 if let Some(ref cb) = callback {
137 Python::attach(|py| {
138 let py_bytes = PyBytes::new(py, &data);
139 let _ = cb.call1(py, (msg_type, py_bytes, hot_x, hot_y));
140 });
141 }
142 }
143 });
144 tx
145}
146
147struct CappedSprite {
151 png: Vec<u8>,
152 scale: f64,
153 width: u32,
154 height: u32,
155}
156
157const SPRITE_CACHE_MAX: usize = 100;
159
160fn cap_and_encode(mut img: RgbaImage, cap: i32) -> Option<CappedSprite> {
167 let (w, h) = (img.width(), img.height());
168 if w == 0 || h == 0 {
169 return None;
170 }
171 let longest = w.max(h);
172 let mut scale = 1.0;
173 if cap > 0 && longest > cap as u32 {
174 scale = cap as f64 / longest as f64;
175 let nw = ((w as f64 * scale).round() as u32).max(1);
176 let nh = ((h as f64 * scale).round() as u32).max(1);
177 img = image::imageops::resize(&img, nw, nh, image::imageops::FilterType::Lanczos3);
178 }
179 crate::unpremultiply_rgba(&mut img);
180 let mut png = Vec::new();
181 img.write_to(&mut IoCursor::new(&mut png), image::ImageFormat::Png)
182 .ok()?;
183 Some(CappedSprite {
184 png,
185 scale,
186 width: img.width(),
187 height: img.height(),
188 })
189}
190
191fn scaled_hotspot(sprite: &CappedSprite, hot_x: i32, hot_y: i32) -> (i32, i32) {
195 (
196 ((hot_x as f64 * sprite.scale).round() as i32).clamp(0, sprite.width as i32 - 1),
197 ((hot_y as f64 * sprite.scale).round() as i32).clamp(0, sprite.height as i32 - 1),
198 )
199}
200
201fn capped_job(
205 cache: &mut HashMap<(u64, i32), CappedSprite>,
206 hash: u64,
207 cap: i32,
208 hot_x: i32,
209 hot_y: i32,
210 decode: impl FnOnce() -> Option<RgbaImage>,
211) -> (&'static str, Vec<u8>, i32, i32) {
212 use std::collections::hash_map::Entry;
213 let key = (hash, cap);
214 if cache.len() >= SPRITE_CACHE_MAX
215 && !cache.contains_key(&key)
216 && let Some(&evict) = cache.keys().next()
217 {
218 cache.remove(&evict);
219 }
220 let sprite = match cache.entry(key) {
221 Entry::Occupied(e) => e.into_mut(),
222 Entry::Vacant(e) => match decode().and_then(|img| cap_and_encode(img, cap)) {
225 Some(sprite) => e.insert(sprite),
226 None => return ("png", Vec::new(), 0, 0),
227 },
228 };
229 let (sx, sy) = scaled_hotspot(sprite, hot_x, hot_y);
230 ("png", sprite.png.clone(), sx, sy)
231}
232
233const MAX_SPRITE_SIDE: i32 = 1024;
237
238fn decode_shm_cursor(
243 width: i32,
244 height: i32,
245 stride: i32,
246 offset: i32,
247 opaque: bool,
248 raw_bytes: &[u8],
249) -> Option<RgbaImage> {
250 if width <= 0
251 || height <= 0
252 || width > MAX_SPRITE_SIDE
253 || height > MAX_SPRITE_SIDE
254 || raw_bytes.is_empty()
255 {
256 return None;
257 }
258 let mut img_buf = ImageBuffer::<Rgba<u8>, Vec<u8>>::new(width as u32, height as u32);
259 let stride_usize = stride.max(0) as usize;
260 let base_offset = offset.max(0) as usize;
261 for y in 0..(height as u32) {
262 for x in 0..(width as u32) {
263 let offset = (y as usize)
264 .checked_mul(stride_usize)
265 .and_then(|row| base_offset.checked_add(row))
266 .and_then(|o| o.checked_add((x as usize) * 4));
267 let offset = match offset {
268 Some(o) => o,
269 None => continue,
270 };
271 if offset
272 .checked_add(4)
273 .is_some_and(|end| end <= raw_bytes.len())
274 {
275 let alpha = if opaque { 255 } else { raw_bytes[offset + 3] };
276 img_buf.put_pixel(
277 x,
278 y,
279 Rgba([
280 raw_bytes[offset + 2],
281 raw_bytes[offset + 1],
282 raw_bytes[offset],
283 alpha,
284 ]),
285 );
286 }
287 }
288 }
289 Some(img_buf)
290}
291
292fn decode_gles_cursor(width: i32, height: i32, raw_bytes: &[u8]) -> Option<RgbaImage> {
295 if width <= 0
296 || height <= 0
297 || width > MAX_SPRITE_SIDE
298 || height > MAX_SPRITE_SIDE
299 || raw_bytes.is_empty()
300 {
301 return None;
302 }
303 let stride =
304 super::frontend::rgba_readback_stride(raw_bytes.len(), height as usize, width as usize);
305 let mut img_buf = ImageBuffer::<Rgba<u8>, Vec<u8>>::new(width as u32, height as u32);
306 for y in 0..(height as u32) {
307 for x in 0..(width as u32) {
308 let offset = (y as usize * stride) + (x as usize * 4);
309 if offset + 4 <= raw_bytes.len() {
310 img_buf.put_pixel(
311 x,
312 y,
313 Rgba([
314 raw_bytes[offset],
315 raw_bytes[offset + 1],
316 raw_bytes[offset + 2],
317 raw_bytes[offset + 3],
318 ]),
319 );
320 }
321 }
322 }
323 Some(img_buf)
324}
325
326pub struct Cursor {
332 icons: Vec<Image>,
333 theme: CursorTheme,
334 size: u32,
335}
336
337impl Cursor {
338 pub fn load(size_override: i32) -> Cursor {
344 let name = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into());
345 let size: u32 = if size_override > 0 {
346 size_override as u32
347 } else {
348 24
349 };
350
351 let theme = CursorTheme::load(&name);
352 let icons = load_icon(&theme, "default").unwrap_or_else(|_| {
353 let size = 16;
354 let mut pixels = Vec::with_capacity((size * size * 4) as usize);
355 for _ in 0..(size * size) {
356 pixels.extend_from_slice(&[255, 0, 0, 255]);
357 }
358
359 vec![Image {
360 size,
361 width: size,
362 height: size,
363 xhot: 0,
364 yhot: 0,
365 delay: 1,
366 pixels_rgba: pixels,
367 pixels_argb: vec![],
368 }]
369 });
370
371 Cursor { icons, theme, size }
372 }
373
374 pub fn get_image(&self, scale: u32, time: Duration) -> Image {
376 let size = self.size * scale;
377 frame(time.as_millis() as u32, size, &self.icons)
378 }
379
380 pub fn get_image_by_name(&self, name: &str, scale: u32, time: Duration) -> Option<Image> {
382 let icons = load_icon(&self.theme, name).ok()?;
383 let size = self.size * scale;
384 Some(frame(time.as_millis() as u32, size, &icons))
385 }
386
387 pub fn get_sprite(&self, name: &str) -> Option<(RgbaImage, u32, u32)> {
392 let icons = load_icon(&self.theme, name).ok()?;
393 let image_data = nearest_images(self.size, &icons).next()?;
394
395 let img_buf: RgbaImage = ImageBuffer::from_raw(
396 image_data.width,
397 image_data.height,
398 image_data.pixels_rgba.clone(),
399 )?;
400
401 Some((img_buf, image_data.xhot, image_data.yhot))
402 }
403}
404
405fn nearest_images(size: u32, images: &[Image]) -> impl Iterator<Item = &Image> {
409 let nearest_image = images
410 .iter()
411 .min_by_key(|image| (size as i32 - image.size as i32).abs())
412 .unwrap();
413 images.iter().filter(move |image| {
414 image.width == nearest_image.width && image.height == nearest_image.height
415 })
416}
417
418fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image {
422 let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay);
423 if total == 0 {
424 return nearest_images(size, images).next().unwrap().clone();
425 }
426 millis %= total;
427 for img in nearest_images(size, images) {
428 if millis < img.delay {
429 return img.clone();
430 }
431 millis -= img.delay;
432 }
433 unreachable!()
434}
435
436fn load_icon(theme: &CursorTheme, name: &str) -> Result<Vec<Image>, String> {
441 let icon_path = theme.load_icon(name).ok_or("Icon not found")?;
442 let mut cursor_file = std::fs::File::open(icon_path).map_err(|e| e.to_string())?;
443 let mut cursor_data = Vec::new();
444 cursor_file
445 .read_to_end(&mut cursor_data)
446 .map_err(|e| e.to_string())?;
447 let imgs = parse_xcursor(&cursor_data).ok_or("Failed to parse".to_string())?;
448 if imgs.is_empty() {
449 return Err("Cursor file has no images".to_string());
450 }
451 Ok(imgs)
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn a_large_cursor_surface_is_read_and_capped() {
460 let side = 256;
463 let raw = vec![0xffu8; (side * side * 4) as usize];
464 let img = decode_shm_cursor(side, side, side * 4, 0, false, &raw).unwrap();
465 assert_eq!((img.width(), img.height()), (256, 256));
466 let sprite = cap_and_encode(img, 128).unwrap();
467 let png = image::load_from_memory(&sprite.png).unwrap();
468 assert_eq!((png.width(), png.height()), (128, 128));
469 assert!(decode_shm_cursor(2048, 2048, 2048 * 4, 0, false, &raw).is_none());
470 assert!(decode_gles_cursor(2048, 2048, &raw).is_none());
471 }
472
473 #[test]
474 fn cap_keeps_a_sprite_within_it_and_shrinks_one_past_it() {
475 let whole = RgbaImage::from_pixel(128, 128, image::Rgba([0, 0, 0, 255]));
476 let sprite = cap_and_encode(whole, 128).unwrap();
477 let img = image::load_from_memory(&sprite.png).unwrap();
478 assert_eq!((img.width(), img.height()), (128, 128));
479 let big = RgbaImage::from_pixel(256, 64, image::Rgba([0, 0, 0, 255]));
480 let sprite = cap_and_encode(big, 128).unwrap();
481 let img = image::load_from_memory(&sprite.png).unwrap();
482 assert_eq!((img.width(), img.height()), (128, 32));
483 }
484}