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