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
11use waybackend::{Waybackend, types::ObjectId};
12
13#[derive(Debug)]
14pub enum CursorLoadError {
15    Io(rustix::io::Errno),
16    EnvVar(std::env::VarError),
17}
18
19impl core::error::Error for CursorLoadError {
20    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
21        match self {
22            CursorLoadError::Io(errno) => errno.source(),
23            CursorLoadError::EnvVar(var_error) => var_error.source(),
24        }
25    }
26
27    fn cause(&self) -> Option<&dyn core::error::Error> {
28        self.source()
29    }
30}
31
32impl core::fmt::Display for CursorLoadError {
33    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34        write!(f, "CursorLoadError: ")?;
35        match self {
36            CursorLoadError::Io(errno) => errno.fmt(f),
37            CursorLoadError::EnvVar(var_error) => var_error.fmt(f),
38        }
39    }
40}
41
42pub struct CursorTheme {
43    name: String,
44    cursors: Vec<Cursor>,
45    /// cursor size
46    size: u32,
47    wl_shm_pool: ObjectId,
48    pool_size: u32,
49    pool_len: u32,
50    shm_fd: rustix::fd::OwnedFd,
51}
52
53impl CursorTheme {
54    /// Use this to create the initial pool
55    const INITIAL_POOL_SIZE: usize = 16 * 16 * 4;
56
57    /// Tries to load the cursor theme from the `XCURSOR_THEME` and `XCURSOR_SIZE` environment variables
58    ///
59    /// See [`CursorTheme::load_from_name`] for an example of a `create_pool_fn`.
60    #[inline]
61    pub fn load_from_env<F>(mut size: u32, create_pool_fn: F) -> Result<Self, CursorLoadError>
62    where
63        F: FnOnce(&rustix::fd::OwnedFd, u32) -> ObjectId,
64    {
65        let name = &std::env::var("XCURSOR_THEME").map_err(CursorLoadError::EnvVar)?;
66
67        if let Ok(var) = std::env::var("XCURSOR_SIZE")
68            && let Ok(int) = var.parse()
69        {
70            size = int;
71        }
72
73        Self::load_from_name(name, size, create_pool_fn)
74    }
75
76    /// Tries to load the cursor theme from an `str`
77    ///
78    /// `create_pool_fn` is a function that takes the shm file descriptor and the initial size.
79    /// Typically, it will look like this (note in the example we are loading the generatec wayland
80    /// glue code with `waybackend-scanner` in a module called `wayland`):
81    ///
82    /// ```ignore
83    /// use waybackend_cursor::CursorTheme;
84    /// use waybackend::{types::ObjectId, objman, Waybackend};
85    ///
86    /// #[derive(Clone, Copy, Debug, PartialEq)]
87    /// enum WaylandObject {
88    ///     Display,
89    ///     Shm,
90    ///     ShmPool,
91    ///     Buffer,
92    ///     Surface,
93    ///     // ...
94    /// }
95    ///
96    /// fn load_cursor(
97    ///     backend: &mut Waybackend,
98    ///     objman: &mut objman::ObjectManager::<WaylandObject>,
99    ///     wayland_shm: ObjectId) -> CursorTheme {
100    ///     CursorTheme::load_from_name("default", 24, |fd, size| {
101    ///         let shm_pool = objman.create(WaylandObject::ShmPool);
102    ///         wayland::wl_shm::req::create_pool(
103    ///             backend,
104    ///             wayland_shm,
105    ///             shm_pool,
106    ///             fd,
107    ///             size as i32,
108    ///         )
109    ///         .unwrap();
110    ///         shm_pool
111    ///     }).unwrap()
112    /// }
113    /// ```
114    #[inline]
115    pub fn load_from_name<F>(
116        name: &str,
117        size: u32,
118        create_pool_fn: F,
119    ) -> Result<Self, CursorLoadError>
120    where
121        F: FnOnce(&rustix::fd::OwnedFd, u32) -> ObjectId,
122    {
123        //  Create shm.
124        let shm_fd = waybackend::shm::create().map_err(CursorLoadError::Io)?;
125        rustix::fs::ftruncate(&shm_fd, Self::INITIAL_POOL_SIZE as u64)
126            .map_err(CursorLoadError::Io)?;
127        let wl_shm_pool = create_pool_fn(&shm_fd, size);
128
129        let name = String::from(name);
130        Ok(Self {
131            name,
132            size,
133            pool_size: Self::INITIAL_POOL_SIZE as u32,
134            pool_len: 0,
135            cursors: Vec::new(),
136            wl_shm_pool,
137            shm_fd,
138        })
139    }
140
141    /// Gets a cursor from the theme
142    ///
143    ///  - `backend` will be fed to the two functions.
144    ///  - `create_buffer_fn` is a closure that returns  a `ObjectId`: the ObjectId of the
145    ///    newly created wayland buffer. It accepts 6 inputs:
146    ///    * backend (should be `waybackend::Waybackend`)
147    ///    * the `wl_shm_pool` ObjectId
148    ///    * offset (see the wayland protocol documentation)
149    ///    * width  (see the wayland protocol documentation)
150    ///    * height (see the wayland protocol documentation)
151    ///    * stride (see the wayland protocol documentation)
152    ///  - `resize_fn` is a closure that resizes the `wl_shm_pool`. It accepts 3 inputs:
153    ///    * the backend (should be `waybackend::Waybackend`)
154    ///    * the `wl_shm_pool` ObjectId
155    ///    * the new size
156    ///
157    /// Here is an example of what this function call could look like:
158    ///
159    /// ```ignore
160    /// use waybackend::{types::ObjectId, objman::Objman, Waybackend};
161    /// use waybackend_cursor::{Cursor, CursorTheme};
162    /// fn get_cursor(
163    ///     cursor_theme: &mut CursorTheme,
164    ///     backend: &mut Waybackend,
165    ///     objman: &mut Objman,
166    ///     name: &str,
167    /// ) -> Option<&Cursor> {
168    ///     cursor_theme.get_cursor(
169    ///         name,
170    ///         backend,
171    ///         |backend, pool, offset, width, height, stride| {
172    ///             // NOTE: you probably also want to store this buffer id somewhere
173    ///             let buffer = objman.create(WaylandObject::Buffer);
174    ///             wayland::wl_shm_pool::req::create_buffer(
175    ///                 backend,
176    ///                 pool,
177    ///                 buffer,
178    ///                 offset,
179    ///                 width,
180    ///                 height,
181    ///                 stride,
182    ///                 wayland::wl_shm::Format::argb8888,
183    ///             )
184    ///             .unwrap();
185    ///             buffer
186    ///         },
187    ///         |backend, id, size| {
188    ///             wayland::wl_shm_pool::req::resize(backend, id, size as i32).unwrap()
189    ///         },
190    ///     )
191    /// }
192    /// ```
193    #[inline]
194    pub fn get_cursor<F1, F2>(
195        &mut self,
196        name: &str,
197        backend: &mut Waybackend,
198        create_buffer_fn: F1,
199        resize_fn: F2,
200    ) -> Option<&Cursor>
201    where
202        F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
203        F2: FnMut(&mut Waybackend, ObjectId, u32),
204    {
205        match self.cursors.iter().position(|cursor| cursor.name == name) {
206            Some(i) => Some(&self.cursors[i]),
207            None => {
208                let cursor =
209                    self.load_cursor(name, self.size, backend, create_buffer_fn, resize_fn)?;
210                self.cursors.push(cursor);
211                self.cursors.last()
212            }
213        }
214    }
215
216    #[inline]
217    fn load_cursor<F1, F2>(
218        &mut self,
219        name: &str,
220        size: u32,
221        backend: &mut Waybackend,
222        create_buffer_fn: F1,
223        resize_fn: F2,
224    ) -> Option<Cursor>
225    where
226        F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
227        F2: FnMut(&mut Waybackend, ObjectId, u32),
228    {
229        use rustix::{buffer, fs, io};
230        let icon_path = xcursor::CursorTheme::load(&self.name).load_icon(name)?;
231        let icon_file = fs::open(icon_path, fs::OFlags::RDONLY, fs::Mode::RUSR).ok()?;
232
233        let mut buf = Vec::with_capacity(4096);
234        loop {
235            match io::retry_on_intr(|| io::read(&icon_file, buffer::spare_capacity(&mut buf))) {
236                Ok(0) => break,
237                Err(_) => return None,
238                Ok(_) => (),
239            }
240
241            if buf.capacity() == buf.len() {
242                buf.reserve(buf.capacity() * 2);
243            }
244        }
245        let images = xcursor::parser::parse_xcursor(&buf)?;
246
247        Some(Cursor::new(
248            name,
249            self,
250            &images,
251            size,
252            backend,
253            create_buffer_fn,
254            resize_fn,
255        ))
256    }
257
258    #[inline]
259    fn grow<F>(&mut self, backend: &mut Waybackend, size: u32, resize_fn: F)
260    where
261        F: FnOnce(&mut Waybackend, ObjectId, u32),
262    {
263        if size > self.pool_size {
264            rustix::fs::ftruncate(&self.shm_fd, size as u64)
265                .expect("failed to new cursor buffer length");
266            resize_fn(backend, self.wl_shm_pool, size);
267            self.pool_size = size;
268        }
269    }
270}
271
272pub struct Cursor {
273    name: String,
274    images: Vec<CursorImageBuffer>,
275    total_duration: u32,
276}
277
278impl Cursor {
279    #[inline]
280    fn new<F1, F2>(
281        name: &str,
282        theme: &mut CursorTheme,
283        images: &[xcursor::parser::Image],
284        size: u32,
285        backend: &mut Waybackend,
286        mut create_buffer_fn: F1,
287        mut resize_fn: F2,
288    ) -> Self
289    where
290        F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
291        F2: FnMut(&mut Waybackend, ObjectId, u32),
292    {
293        let mut total_duration = 0;
294        let f1 = &mut create_buffer_fn;
295        let f2 = &mut resize_fn;
296        let images: Vec<CursorImageBuffer> = Self::nearest_images(size, images)
297            .map(|image| {
298                let buffer =
299                    CursorImageBuffer::new::<&mut F1, &mut F2>(theme, image, backend, f1, f2);
300                total_duration += buffer.delay;
301
302                buffer
303            })
304            .collect();
305
306        Self {
307            total_duration,
308            name: String::from(name),
309            images,
310        }
311    }
312
313    #[inline]
314    fn nearest_images(
315        size: u32,
316        images: &[xcursor::parser::Image],
317    ) -> impl Iterator<Item = &xcursor::parser::Image> {
318        // Follow the nominal size of the cursor to choose the nearest
319        let nearest_image = images
320            .iter()
321            .min_by_key(|image| (size as i32 - image.size as i32).abs())
322            .unwrap();
323        images.iter().filter(move |image| {
324            image.width == nearest_image.width && image.height == nearest_image.height
325        })
326    }
327
328    /// Time is returned in milliseconds
329    #[inline]
330    pub fn frame_and_duration(&self, mut millis: u32) -> (usize, u32) {
331        millis %= self.total_duration;
332        let mut res = 0;
333        for (i, img) in self.images.iter().enumerate() {
334            if millis < img.delay {
335                res = i;
336                break;
337            }
338            millis -= img.delay;
339        }
340
341        (res, millis)
342    }
343
344    #[inline]
345    pub fn images(&self) -> &[CursorImageBuffer] {
346        &self.images
347    }
348}
349
350pub struct CursorImageBuffer {
351    wl_buffer: ObjectId,
352    delay: u32,
353    xhot: u32,
354    yhot: u32,
355    width: u32,
356    height: u32,
357}
358
359impl CursorImageBuffer {
360    /// Construct a new CursorImageBuffer
361    ///
362    /// This function appends the pixels of the image to the provided file,
363    /// and constructs a wl_buffer on that data.
364    #[inline]
365    fn new<F1, F2>(
366        theme: &mut CursorTheme,
367        image: &xcursor::parser::Image,
368        backend: &mut Waybackend,
369        create_buffer_fn: F1,
370        resize_fn: F2,
371    ) -> Self
372    where
373        F1: FnOnce(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
374        F2: FnOnce(&mut Waybackend, ObjectId, u32),
375    {
376        use rustix::mm::{MapFlags, ProtFlags, mmap, munmap};
377        let buf = &image.pixels_rgba;
378        let offset = theme.pool_len as u64;
379        // Resize memory before writing to it to handle shm correctly.
380        let new_size = offset + buf.len() as u64;
381        theme.grow(backend, new_size as u32, resize_fn);
382        let mmap = unsafe {
383            mmap(
384                core::ptr::null_mut(),
385                new_size as usize,
386                ProtFlags::READ | ProtFlags::WRITE,
387                MapFlags::SHARED,
388                &theme.shm_fd,
389                0,
390            )
391        }
392        .expect("failed to mmap cursor shared memory");
393        {
394            let slice = unsafe {
395                core::slice::from_raw_parts_mut(mmap.cast::<u8>().add(offset as usize), buf.len())
396            };
397            slice.copy_from_slice(buf);
398        }
399        theme.pool_len += buf.len() as u32;
400
401        let wl_buffer = create_buffer_fn(
402            backend,
403            theme.wl_shm_pool,
404            offset as i32,
405            image.width as i32,
406            image.height as i32,
407            (image.width * 4) as i32,
408        );
409
410        unsafe { munmap(mmap, new_size as usize).expect("failed to munmap cursor shared memory") };
411        Self {
412            wl_buffer,
413            delay: image.delay,
414            xhot: image.xhot,
415            yhot: image.yhot,
416            width: image.width,
417            height: image.height,
418        }
419    }
420
421    /// Dimensions of this image
422    #[inline]
423    pub fn dimensions(&self) -> (u32, u32) {
424        (self.width, self.height)
425    }
426
427    /// Location of the pointer hotspot in this image
428    #[inline]
429    pub fn hotspot(&self) -> (u32, u32) {
430        (self.xhot, self.yhot)
431    }
432
433    /// Time (in milliseconds) for which this image should be displayed
434    #[inline]
435    pub fn delay(&self) -> u32 {
436        self.delay
437    }
438
439    /// The wayland buffer
440    #[inline]
441    pub fn wl_buffer(&self) -> ObjectId {
442        self.wl_buffer
443    }
444}