1use waybackend::{Waybackend, types::ObjectId};
13
14#[derive(Debug)]
15pub enum CursorLoadError {
16    Io(rustix::io::Errno),
17    EnvVar(std::env::VarError),
18}
19
20impl std::error::Error for CursorLoadError {
21    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
22        match self {
23            CursorLoadError::Io(errno) => errno.source(),
24            CursorLoadError::EnvVar(var_error) => var_error.source(),
25        }
26    }
27
28    fn cause(&self) -> Option<&dyn std::error::Error> {
29        self.source()
30    }
31}
32
33impl std::fmt::Display for CursorLoadError {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        write!(f, "CursorLoadError: ")?;
36        match self {
37            CursorLoadError::Io(errno) => errno.fmt(f),
38            CursorLoadError::EnvVar(var_error) => var_error.fmt(f),
39        }
40    }
41}
42
43pub struct CursorTheme {
44    name: String,
45    cursors: Vec<Cursor>,
46    size: u32,
48    wl_shm_pool: ObjectId,
49    pool_size: u32,
50    pool_len: u32,
51    shm_fd: rustix::fd::OwnedFd,
52}
53
54impl CursorTheme {
55    const INITIAL_POOL_SIZE: usize = 16 * 16 * 4;
57
58    #[inline]
62    pub fn load_from_env<F>(mut size: u32, create_pool_fn: F) -> Result<Self, CursorLoadError>
63    where
64        F: FnOnce(&rustix::fd::OwnedFd, u32) -> ObjectId,
65    {
66        let name = &std::env::var("XCURSOR_THEME").map_err(CursorLoadError::EnvVar)?;
67
68        if let Ok(var) = std::env::var("XCURSOR_SIZE")
69            && let Ok(int) = var.parse()
70        {
71            size = int;
72        }
73
74        Self::load_from_name(name, size, create_pool_fn)
75    }
76
77    #[inline]
116    pub fn load_from_name<F>(
117        name: &str,
118        size: u32,
119        create_pool_fn: F,
120    ) -> Result<Self, CursorLoadError>
121    where
122        F: FnOnce(&rustix::fd::OwnedFd, u32) -> ObjectId,
123    {
124        let shm_fd = waybackend::shm::create().map_err(CursorLoadError::Io)?;
126        rustix::fs::ftruncate(&shm_fd, Self::INITIAL_POOL_SIZE as u64)
127            .map_err(CursorLoadError::Io)?;
128        let wl_shm_pool = create_pool_fn(&shm_fd, size);
129
130        let name = String::from(name);
131        Ok(Self {
132            name,
133            size,
134            pool_size: Self::INITIAL_POOL_SIZE as u32,
135            pool_len: 0,
136            cursors: Vec::new(),
137            wl_shm_pool,
138            shm_fd,
139        })
140    }
141
142    #[inline]
195    pub fn get_cursor<F1, F2>(
196        &mut self,
197        name: &str,
198        backend: &mut Waybackend,
199        create_buffer_fn: F1,
200        resize_fn: F2,
201    ) -> Option<&Cursor>
202    where
203        F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
204        F2: FnMut(&mut Waybackend, ObjectId, u32),
205    {
206        match self.cursors.iter().position(|cursor| cursor.name == name) {
207            Some(i) => Some(&self.cursors[i]),
208            None => {
209                let cursor =
210                    self.load_cursor(name, self.size, backend, create_buffer_fn, resize_fn)?;
211                self.cursors.push(cursor);
212                self.cursors.iter().last()
213            }
214        }
215    }
216
217    #[inline]
218    fn load_cursor<F1, F2>(
219        &mut self,
220        name: &str,
221        size: u32,
222        backend: &mut Waybackend,
223        create_buffer_fn: F1,
224        resize_fn: F2,
225    ) -> Option<Cursor>
226    where
227        F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
228        F2: FnMut(&mut Waybackend, ObjectId, u32),
229    {
230        let icon_path = xcursor::CursorTheme::load(&self.name).load_icon(name)?;
231        let mut icon_file = std::fs::File::open(icon_path).ok()?;
232
233        let mut buf = Vec::new();
234        let images = {
235            std::io::Read::read_to_end(&mut icon_file, &mut buf).ok()?;
236            xcursor::parser::parse_xcursor(&buf)?
237        };
238
239        Some(Cursor::new(
240            name,
241            self,
242            &images,
243            size,
244            backend,
245            create_buffer_fn,
246            resize_fn,
247        ))
248    }
249
250    #[inline]
251    fn grow<F>(&mut self, backend: &mut Waybackend, size: u32, resize_fn: F)
252    where
253        F: FnOnce(&mut Waybackend, ObjectId, u32),
254    {
255        if size > self.pool_size {
256            rustix::fs::ftruncate(&self.shm_fd, size as u64)
257                .expect("failed to new cursor buffer length");
258            resize_fn(backend, self.wl_shm_pool, size);
259            self.pool_size = size;
260        }
261    }
262}
263
264pub struct Cursor {
265    name: String,
266    images: Vec<CursorImageBuffer>,
267    total_duration: u32,
268}
269
270impl Cursor {
271    #[inline]
272    fn new<F1, F2>(
273        name: &str,
274        theme: &mut CursorTheme,
275        images: &[xcursor::parser::Image],
276        size: u32,
277        backend: &mut Waybackend,
278        mut create_buffer_fn: F1,
279        mut resize_fn: F2,
280    ) -> Self
281    where
282        F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
283        F2: FnMut(&mut Waybackend, ObjectId, u32),
284    {
285        let mut total_duration = 0;
286        let f1 = &mut create_buffer_fn;
287        let f2 = &mut resize_fn;
288        let images: Vec<CursorImageBuffer> = Self::nearest_images(size, images)
289            .map(|image| {
290                let buffer =
291                    CursorImageBuffer::new::<&mut F1, &mut F2>(theme, image, backend, f1, f2);
292                total_duration += buffer.delay;
293
294                buffer
295            })
296            .collect();
297
298        Self {
299            total_duration,
300            name: String::from(name),
301            images,
302        }
303    }
304
305    #[inline]
306    fn nearest_images(
307        size: u32,
308        images: &[xcursor::parser::Image],
309    ) -> impl Iterator<Item = &xcursor::parser::Image> {
310        let nearest_image = images
312            .iter()
313            .min_by_key(|image| (size as i32 - image.size as i32).abs())
314            .unwrap();
315        images.iter().filter(move |image| {
316            image.width == nearest_image.width && image.height == nearest_image.height
317        })
318    }
319
320    #[inline]
322    pub fn frame_and_duration(&self, mut millis: u32) -> (usize, u32) {
323        millis %= self.total_duration;
324        let mut res = 0;
325        for (i, img) in self.images.iter().enumerate() {
326            if millis < img.delay {
327                res = i;
328                break;
329            }
330            millis -= img.delay;
331        }
332
333        (res, millis)
334    }
335
336    #[inline]
337    pub fn images(&self) -> &[CursorImageBuffer] {
338        &self.images
339    }
340}
341
342pub struct CursorImageBuffer {
343    wl_buffer: ObjectId,
344    delay: u32,
345    xhot: u32,
346    yhot: u32,
347    width: u32,
348    height: u32,
349}
350
351impl CursorImageBuffer {
352    #[inline]
357    fn new<F1, F2>(
358        theme: &mut CursorTheme,
359        image: &xcursor::parser::Image,
360        backend: &mut Waybackend,
361        create_buffer_fn: F1,
362        resize_fn: F2,
363    ) -> Self
364    where
365        F1: FnOnce(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
366        F2: FnOnce(&mut Waybackend, ObjectId, u32),
367    {
368        use rustix::mm::{MapFlags, ProtFlags, mmap, munmap};
369        let buf = &image.pixels_rgba;
370        let offset = theme.pool_len as u64;
371        let new_size = offset + buf.len() as u64;
373        theme.grow(backend, new_size as u32, resize_fn);
374        let mmap = unsafe {
375            mmap(
376                std::ptr::null_mut(),
377                new_size as usize,
378                ProtFlags::READ | ProtFlags::WRITE,
379                MapFlags::SHARED,
380                &theme.shm_fd,
381                0,
382            )
383        }
384        .expect("failed to mmap cursor shared memory");
385        {
386            let slice = unsafe {
387                std::slice::from_raw_parts_mut(mmap.cast::<u8>().add(offset as usize), buf.len())
388            };
389            slice.copy_from_slice(buf);
390        }
391        theme.pool_len += buf.len() as u32;
392
393        let wl_buffer = create_buffer_fn(
394            backend,
395            theme.wl_shm_pool,
396            offset as i32,
397            image.width as i32,
398            image.height as i32,
399            (image.width * 4) as i32,
400        );
401
402        unsafe { munmap(mmap, new_size as usize).expect("failed to munmap cursor shared memory") };
403        Self {
404            wl_buffer,
405            delay: image.delay,
406            xhot: image.xhot,
407            yhot: image.yhot,
408            width: image.width,
409            height: image.height,
410        }
411    }
412
413    #[inline]
415    pub fn dimensions(&self) -> (u32, u32) {
416        (self.width, self.height)
417    }
418
419    #[inline]
421    pub fn hotspot(&self) -> (u32, u32) {
422        (self.xhot, self.yhot)
423    }
424
425    #[inline]
427    pub fn delay(&self) -> u32 {
428        self.delay
429    }
430
431    #[inline]
433    pub fn wl_buffer(&self) -> ObjectId {
434        self.wl_buffer
435    }
436}