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 parser::{parse_xcursor, Image},
20 CursorTheme,
21};
22
23pub enum CursorJob {
29 SetCallback(Py<PyAny>),
30 SetSize(i32),
32 SetSizeCap(i32),
35 Named { name: &'static str },
36 Hide,
37 Shm {
39 hash: u64,
40 width: i32,
41 height: i32,
42 stride: i32,
43 offset: i32,
44 opaque: bool,
45 bytes: Vec<u8>,
46 hot_x: i32,
47 hot_y: i32,
48 },
49 Gles {
51 hash: u64,
52 width: i32,
53 height: i32,
54 bytes: Vec<u8>,
55 hot_x: i32,
56 hot_y: i32,
57 },
58}
59
60pub fn spawn_cursor_worker(cursor_size: i32, size_cap: i32) -> std::sync::mpsc::Sender<CursorJob> {
64 let (tx, rx) = std::sync::mpsc::channel::<CursorJob>();
65 let _ = std::thread::Builder::new().name("wl-cursor".into()).spawn(move || {
66 let mut helper = Cursor::load(cursor_size);
67 let mut cap = size_cap;
68 let mut cache: HashMap<(u64, i32), CappedSprite> = HashMap::new();
69 let mut callback: Option<Py<PyAny>> = None;
70 while let Ok(job) = rx.recv() {
71 let (msg_type, data, hot_x, hot_y): (&str, Vec<u8>, i32, i32) = match job {
72 CursorJob::SetCallback(cb) => {
73 callback = Some(cb);
74 continue;
75 }
76 CursorJob::SetSize(size) => {
77 helper = Cursor::load(size);
78 continue;
79 }
80 CursorJob::SetSizeCap(c) => {
81 cap = c;
82 continue;
83 }
84 CursorJob::Named { name } => match helper.get_sprite(name) {
85 Some((img, x, y)) => match cap_and_encode(img, cap) {
86 Some(sprite) => {
87 let (sx, sy) = scaled_hotspot(&sprite, x as i32, y as i32);
88 ("png", sprite.png, sx, sy)
89 }
90 None => ("error", Vec::new(), 0, 0),
91 },
92 None => ("error", Vec::new(), 0, 0),
93 },
94 CursorJob::Hide => ("hide", Vec::new(), 0, 0),
95 CursorJob::Shm {
96 hash,
97 width,
98 height,
99 stride,
100 offset,
101 opaque,
102 bytes,
103 hot_x,
104 hot_y,
105 } => capped_job(
106 &mut cache,
107 hash,
108 cap,
109 hot_x,
110 hot_y,
111 || decode_shm_cursor(width, height, stride, offset, opaque, &bytes),
112 ),
113 CursorJob::Gles { hash, width, height, bytes, hot_x, hot_y } => capped_job(
114 &mut cache,
115 hash,
116 cap,
117 hot_x,
118 hot_y,
119 || decode_gles_cursor(width, height, &bytes),
120 ),
121 };
122 if data.is_empty() && msg_type != "hide" {
126 continue;
127 }
128 if crate::PY_SHUTDOWN.load(std::sync::atomic::Ordering::Relaxed) {
129 continue;
130 }
131 if let Some(ref cb) = callback {
132 Python::attach(|py| {
133 let py_bytes = PyBytes::new(py, &data);
134 let _ = cb.call1(py, (msg_type, py_bytes, hot_x, hot_y));
135 });
136 }
137 }
138 });
139 tx
140}
141
142struct CappedSprite {
146 png: Vec<u8>,
147 scale: f64,
148 width: u32,
149 height: u32,
150}
151
152const SPRITE_CACHE_MAX: usize = 100;
154
155fn cap_and_encode(mut img: RgbaImage, cap: i32) -> Option<CappedSprite> {
162 let (w, h) = (img.width(), img.height());
163 if w == 0 || h == 0 {
164 return None;
165 }
166 let longest = w.max(h);
167 let mut scale = 1.0;
168 if cap > 0 && longest > cap as u32 {
169 scale = cap as f64 / longest as f64;
170 let nw = ((w as f64 * scale).round() as u32).max(1);
171 let nh = ((h as f64 * scale).round() as u32).max(1);
172 img = image::imageops::resize(&img, nw, nh, image::imageops::FilterType::Lanczos3);
173 }
174 crate::unpremultiply_rgba(&mut img);
175 let mut png = Vec::new();
176 img.write_to(&mut IoCursor::new(&mut png), image::ImageFormat::Png)
177 .ok()?;
178 Some(CappedSprite { png, scale, width: img.width(), height: img.height() })
179}
180
181fn scaled_hotspot(sprite: &CappedSprite, hot_x: i32, hot_y: i32) -> (i32, i32) {
185 (
186 ((hot_x as f64 * sprite.scale).round() as i32).clamp(0, sprite.width as i32 - 1),
187 ((hot_y as f64 * sprite.scale).round() as i32).clamp(0, sprite.height as i32 - 1),
188 )
189}
190
191fn capped_job(
195 cache: &mut HashMap<(u64, i32), CappedSprite>,
196 hash: u64,
197 cap: i32,
198 hot_x: i32,
199 hot_y: i32,
200 decode: impl FnOnce() -> Option<RgbaImage>,
201) -> (&'static str, Vec<u8>, i32, i32) {
202 use std::collections::hash_map::Entry;
203 let key = (hash, cap);
204 if cache.len() >= SPRITE_CACHE_MAX && !cache.contains_key(&key)
205 && let Some(&evict) = cache.keys().next() {
206 cache.remove(&evict);
207 }
208 let sprite = match cache.entry(key) {
209 Entry::Occupied(e) => e.into_mut(),
210 Entry::Vacant(e) => match decode().and_then(|img| cap_and_encode(img, cap)) {
213 Some(sprite) => e.insert(sprite),
214 None => return ("png", Vec::new(), 0, 0),
215 },
216 };
217 let (sx, sy) = scaled_hotspot(sprite, hot_x, hot_y);
218 ("png", sprite.png.clone(), sx, sy)
219}
220
221fn decode_shm_cursor(
226 width: i32,
227 height: i32,
228 stride: i32,
229 offset: i32,
230 opaque: bool,
231 raw_bytes: &[u8],
232) -> Option<RgbaImage> {
233 if width <= 0 || height <= 0 || width > 128 || height > 128 || raw_bytes.is_empty() {
234 return None;
235 }
236 let mut img_buf = ImageBuffer::<Rgba<u8>, Vec<u8>>::new(width as u32, height as u32);
237 let stride_usize = stride.max(0) as usize;
238 let base_offset = offset.max(0) as usize;
239 for y in 0..(height as u32) {
240 for x in 0..(width as u32) {
241 let offset = (y as usize)
242 .checked_mul(stride_usize)
243 .and_then(|row| base_offset.checked_add(row))
244 .and_then(|o| o.checked_add((x as usize) * 4));
245 let offset = match offset {
246 Some(o) => o,
247 None => continue,
248 };
249 if offset.checked_add(4).is_some_and(|end| end <= raw_bytes.len()) {
250 let alpha = if opaque { 255 } else { raw_bytes[offset + 3] };
251 img_buf.put_pixel(
252 x,
253 y,
254 Rgba([raw_bytes[offset + 2], raw_bytes[offset + 1], raw_bytes[offset], alpha]),
255 );
256 }
257 }
258 }
259 Some(img_buf)
260}
261
262fn decode_gles_cursor(width: i32, height: i32, raw_bytes: &[u8]) -> Option<RgbaImage> {
265 if width <= 0 || height <= 0 || width > 128 || height > 128 || raw_bytes.is_empty() {
266 return None;
267 }
268 let stride = super::frontend::rgba_readback_stride(
269 raw_bytes.len(),
270 height as usize,
271 width as usize,
272 );
273 let mut img_buf = ImageBuffer::<Rgba<u8>, Vec<u8>>::new(width as u32, height as u32);
274 for y in 0..(height as u32) {
275 for x in 0..(width as u32) {
276 let offset = (y as usize * stride) + (x as usize * 4);
277 if offset + 4 <= raw_bytes.len() {
278 img_buf.put_pixel(
279 x,
280 y,
281 Rgba([
282 raw_bytes[offset],
283 raw_bytes[offset + 1],
284 raw_bytes[offset + 2],
285 raw_bytes[offset + 3],
286 ]),
287 );
288 }
289 }
290 }
291 Some(img_buf)
292}
293
294pub struct Cursor {
300 icons: Vec<Image>,
301 theme: CursorTheme,
302 size: u32,
303}
304
305impl Cursor {
306 pub fn load(size_override: i32) -> Cursor {
312 let name = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into());
313 let size: u32 = if size_override > 0 { size_override as u32 } else { 24 };
314
315 let theme = CursorTheme::load(&name);
316 let icons = load_icon(&theme, "default").unwrap_or_else(|_| {
317 let size = 16;
318 let mut pixels = Vec::with_capacity((size * size * 4) as usize);
319 for _ in 0..(size * size) {
320 pixels.extend_from_slice(&[255, 0, 0, 255]);
321 }
322
323 vec![Image {
324 size,
325 width: size,
326 height: size,
327 xhot: 0,
328 yhot: 0,
329 delay: 1,
330 pixels_rgba: pixels,
331 pixels_argb: vec![],
332 }]
333 });
334
335 Cursor { icons, theme, size }
336 }
337
338 pub fn get_image(&self, scale: u32, time: Duration) -> Image {
340 let size = self.size * scale;
341 frame(time.as_millis() as u32, size, &self.icons)
342 }
343
344 pub fn get_image_by_name(&self, name: &str, scale: u32, time: Duration) -> Option<Image> {
346 let icons = load_icon(&self.theme, name).ok()?;
347 let size = self.size * scale;
348 Some(frame(time.as_millis() as u32, size, &icons))
349 }
350
351 pub fn get_sprite(&self, name: &str) -> Option<(RgbaImage, u32, u32)> {
356 let icons = load_icon(&self.theme, name).ok()?;
357 let image_data = nearest_images(self.size, &icons).next()?;
358
359 let img_buf: RgbaImage = ImageBuffer::from_raw(
360 image_data.width,
361 image_data.height,
362 image_data.pixels_rgba.clone(),
363 )?;
364
365 Some((img_buf, image_data.xhot, image_data.yhot))
366 }
367}
368
369fn nearest_images(size: u32, images: &[Image]) -> impl Iterator<Item = &Image> {
373 let nearest_image = images
374 .iter()
375 .min_by_key(|image| (size as i32 - image.size as i32).abs())
376 .unwrap();
377 images
378 .iter()
379 .filter(move |image| image.width == nearest_image.width && image.height == nearest_image.height)
380}
381
382fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image {
386 let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay);
387 if total == 0 {
388 return nearest_images(size, images).next().unwrap().clone();
389 }
390 millis %= total;
391 for img in nearest_images(size, images) {
392 if millis < img.delay {
393 return img.clone();
394 }
395 millis -= img.delay;
396 }
397 unreachable!()
398}
399
400fn load_icon(theme: &CursorTheme, name: &str) -> Result<Vec<Image>, String> {
405 let icon_path = theme.load_icon(name).ok_or("Icon not found")?;
406 let mut cursor_file = std::fs::File::open(icon_path).map_err(|e| e.to_string())?;
407 let mut cursor_data = Vec::new();
408 cursor_file
409 .read_to_end(&mut cursor_data)
410 .map_err(|e| e.to_string())?;
411 let imgs = parse_xcursor(&cursor_data).ok_or("Failed to parse".to_string())?;
412 if imgs.is_empty() {
413 return Err("Cursor file has no images".to_string());
414 }
415 Ok(imgs)
416}