waybackend_cursor/
lib.rs

1//! Wayland cursor implementation using `Waybackend` as the backend
2//!
3//! This crate was make by adapting the code in `wayland-cursor-rs`, from the `smithay` project.
4//!
5//! Key differences:
6//!
7//!  - we do not allow automatically setting cursor fallbacks. You'd have to do something manually
8//!    every time
9//!  - when loading a cursor, you must pass closures for running the wayland specific code. There
10//!    are examples of how to do this in the documentation
11
12use 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    /// cursor size
47    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    /// Use this to create the initial pool
56    const INITIAL_POOL_SIZE: usize = 16 * 16 * 4;
57
58    /// Tries to load the cursor theme from the `XCURSOR_THEME` and `XCURSOR_SIZE` environment variables
59    ///
60    /// See [`CursorTheme::load_from_name`] for an example of a `create_pool_fn`.
61    #[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    /// Tries to load the cursor theme from an `str`
78    ///
79    /// `create_pool_fn` is a function that takes the shm file descriptor and the initial size.
80    /// Typically, it will look like this (note in the example we are loading the generatec wayland
81    /// glue code with `waybackend-scanner` in a module called `wayland`):
82    ///
83    /// ```ignore
84    /// use waybackend_cursor::CursorTheme;
85    /// use waybackend::{types::ObjectId, objman, Waybackend};
86    ///
87    /// #[derive(Clone, Copy, Debug, PartialEq)]
88    /// enum WaylandObject {
89    ///     Display,
90    ///     Shm,
91    ///     ShmPool,
92    ///     Buffer,
93    ///     Surface,
94    ///     // ...
95    /// }
96    ///
97    /// fn load_cursor(
98    ///     backend: &mut Waybackend,
99    ///     objman: &mut objman::ObjectManager::<WaylandObject>,
100    ///     wayland_shm: ObjectId) -> CursorTheme {
101    ///     CursorTheme::load_from_name("default", 24, |fd, size| {
102    ///         let shm_pool = objman.create(WaylandObject::ShmPool);
103    ///         wayland::wl_shm::req::create_pool(
104    ///             backend,
105    ///             wayland_shm,
106    ///             shm_pool,
107    ///             fd,
108    ///             size as i32,
109    ///         )
110    ///         .unwrap();
111    ///         shm_pool
112    ///     }).unwrap()
113    /// }
114    /// ```
115    #[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        //  Create shm.
125        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    /// Gets a cursor from the theme
143    ///
144    ///  - `backend` will be fed to the two functions.
145    ///  - `create_buffer_fn` is a closure that returns  a `ObjectId`: the ObjectId of the
146    ///    newly created wayland buffer. It accepts 6 inputs:
147    ///    * backend (should be `waybackend::Waybackend`)
148    ///    * the `wl_shm_pool` ObjectId
149    ///    * offset (see the wayland protocol documentation)
150    ///    * width  (see the wayland protocol documentation)
151    ///    * height (see the wayland protocol documentation)
152    ///    * stride (see the wayland protocol documentation)
153    ///  - `resize_fn` is a closure that resizes the `wl_shm_pool`. It accepts 3 inputs:
154    ///    * the backend (should be `waybackend::Waybackend`)
155    ///    * the `wl_shm_pool` ObjectId
156    ///    * the new size
157    ///
158    /// Here is an example of what this function call could look like:
159    ///
160    /// ```ignore
161    /// use waybackend::{types::ObjectId, objman::Objman, Waybackend};
162    /// use waybackend_cursor::{Cursor, CursorTheme};
163    /// fn get_cursor(
164    ///     cursor_theme: &mut CursorTheme,
165    ///     backend: &mut Waybackend,
166    ///     objman: &mut Objman,
167    ///     name: &str,
168    /// ) -> Option<&Cursor> {
169    ///     cursor_theme.get_cursor(
170    ///         name,
171    ///         backend,
172    ///         |backend, pool, offset, width, height, stride| {
173    ///             // NOTE: you probably also want to store this buffer id somewhere
174    ///             let buffer = objman.create(WaylandObject::Buffer);
175    ///             wayland::wl_shm_pool::req::create_buffer(
176    ///                 backend,
177    ///                 pool,
178    ///                 buffer,
179    ///                 offset,
180    ///                 width,
181    ///                 height,
182    ///                 stride,
183    ///                 wayland::wl_shm::Format::argb8888,
184    ///             )
185    ///             .unwrap();
186    ///             buffer
187    ///         },
188    ///         |backend, id, size| {
189    ///             wayland::wl_shm_pool::req::resize(backend, id, size as i32).unwrap()
190    ///         },
191    ///     )
192    /// }
193    /// ```
194    #[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        // Follow the nominal size of the cursor to choose the nearest
311        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    /// Time is returned in milliseconds
321    #[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    /// Construct a new CursorImageBuffer
353    ///
354    /// This function appends the pixels of the image to the provided file,
355    /// and constructs a wl_buffer on that data.
356    #[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        // Resize memory before writing to it to handle shm correctly.
372        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    /// Dimensions of this image
414    #[inline]
415    pub fn dimensions(&self) -> (u32, u32) {
416        (self.width, self.height)
417    }
418
419    /// Location of the pointer hotspot in this image
420    #[inline]
421    pub fn hotspot(&self) -> (u32, u32) {
422        (self.xhot, self.yhot)
423    }
424
425    /// Time (in milliseconds) for which this image should be displayed
426    #[inline]
427    pub fn delay(&self) -> u32 {
428        self.delay
429    }
430
431    /// The wayland buffer
432    #[inline]
433    pub fn wl_buffer(&self) -> ObjectId {
434        self.wl_buffer
435    }
436}