wayland_cursor/
lib.rs

1#![warn(missing_docs, missing_debug_implementations)]
2#![forbid(improper_ctypes, unsafe_op_in_unsafe_fn)]
3
4//! Wayland cursor utilities
5//!
6//! This crate aims to re-implement the functionality of the `libwayland-cursor` library in Rust.
7//!
8//! It allows you to load cursors from the system and display them correctly.
9//!
10//! First of all, you need to create a [`CursorTheme`], which represents the full cursor theme.
11//!
12//! From this theme, using the [`get_cursor()`][CursorTheme::get_cursor()] method, you can load a
13//! specific [`Cursor`], which can contain several images if the cursor is animated. It also provides
14//! you with the means of querying which frame of the animation should be displayed at what time, as
15//! well as handles to the buffers containing these frames, to attach them to a wayland surface.
16//!
17//! # Example
18//!
19//! ```
20//! use wayland_cursor::CursorTheme;
21//! # use std::ops::Deref;
22//! # use std::thread::sleep;
23//! # use std::time::{Instant, Duration};
24//! # fn test(connection: &wayland_client::Connection, cursor_surface: &wayland_client::protocol::wl_surface::WlSurface, shm: wayland_client::protocol::wl_shm::WlShm) {
25//! // Load the default cursor theme.
26//! let mut cursor_theme = CursorTheme::load(&connection, shm, 32)
27//!     .expect("Could not load cursor theme");
28//! let cursor = cursor_theme.get_cursor("wait")
29//!     .expect("Cursor not provided by theme");
30//!
31//! let start_time = Instant::now();
32//! loop {
33//!     // Obtain which frame we should show, and for how long.
34//!     let millis = start_time.elapsed().as_millis();
35//!     let fr_info = cursor.frame_and_duration(millis as u32);
36//!
37//!     // Here, we obtain the right cursor frame...
38//!     let buffer = &cursor[fr_info.frame_index];
39//!     // and attach it to a wl_surface.
40//!     cursor_surface.attach(Some(&buffer), 0, 0);
41//!     cursor_surface.commit();
42//!
43//!     sleep(Duration::from_millis(fr_info.frame_duration as u64));
44//! }
45//! # }
46//! ```
47
48use std::borrow::Cow;
49use std::env;
50use std::fmt::Debug;
51use std::fs::File;
52use std::io::{Error as IoError, Read, Result as IoResult, Seek, SeekFrom, Write};
53use std::ops::{Deref, Index};
54use std::os::unix::io::{AsFd, OwnedFd};
55use std::sync::Arc;
56use std::time::{SystemTime, UNIX_EPOCH};
57
58use rustix::fs::Mode;
59#[cfg(any(target_os = "linux", target_os = "android"))]
60use rustix::fs::{memfd_create, MemfdFlags};
61use rustix::io::Errno;
62use rustix::shm::{shm_open, shm_unlink, ShmOFlags};
63#[cfg(any(target_os = "linux", target_os = "android"))]
64use std::ffi::CStr;
65
66use wayland_client::backend::{InvalidId, ObjectData, WeakBackend};
67use wayland_client::protocol::wl_buffer::WlBuffer;
68use wayland_client::protocol::wl_shm::{self, Format, WlShm};
69use wayland_client::protocol::wl_shm_pool::{self, WlShmPool};
70use wayland_client::{Connection, Proxy, WEnum};
71
72use xcursor::parser as xparser;
73use xcursor::CursorTheme as XCursorTheme;
74use xparser::Image as XCursorImage;
75
76/// Represents a cursor theme loaded from the system.
77#[derive(Debug)]
78pub struct CursorTheme {
79    name: String,
80    cursors: Vec<Cursor>,
81    size: u32,
82    pool: WlShmPool,
83    pool_size: i32,
84    file: File,
85    backend: WeakBackend,
86    fallback: Option<FallBack>,
87}
88
89type FallBackInner = Box<dyn Fn(&str, u32) -> Option<Cow<'static, [u8]>> + Send + Sync>;
90
91struct FallBack(FallBackInner);
92
93impl FallBack {
94    fn new<F>(fallback: F) -> Self
95    where
96        F: Fn(&str, u32) -> Option<Cow<'static, [u8]>> + Send + Sync + 'static,
97    {
98        Self(Box::new(fallback))
99    }
100}
101
102impl Debug for FallBack {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.write_str("fallback function")
105    }
106}
107
108impl CursorTheme {
109    /// Load a cursor theme from system defaults.
110    ///
111    /// Same as calling the following:
112    /// ```
113    /// # use wayland_cursor::CursorTheme;
114    /// # use wayland_client::{Connection, backend::InvalidId, protocol::wl_shm};
115    /// # fn example(conn: &Connection, shm: wl_shm::WlShm, size: u32) -> Result<CursorTheme, InvalidId> {
116    /// CursorTheme::load_or(conn, shm, "default", size)
117    /// # }
118    /// ```
119    pub fn load(conn: &Connection, shm: WlShm, size: u32) -> Result<Self, InvalidId> {
120        Self::load_or(conn, shm, "default", size)
121    }
122
123    /// Load a cursor theme, using `name` as fallback.
124    ///
125    /// The theme name and cursor size are read from the `XCURSOR_THEME` and
126    /// `XCURSOR_SIZE` environment variables, respectively, or from the provided variables
127    /// if those are invalid.
128    pub fn load_or(
129        conn: &Connection,
130        shm: WlShm,
131        name: &str,
132        mut size: u32,
133    ) -> Result<Self, InvalidId> {
134        let name_string = String::from(name);
135        let name = &env::var("XCURSOR_THEME").unwrap_or(name_string);
136
137        if let Ok(var) = env::var("XCURSOR_SIZE") {
138            if let Ok(int) = var.parse() {
139                size = int;
140            }
141        }
142
143        Self::load_from_name(conn, shm, name, size)
144    }
145
146    /// Create a new cursor theme, ignoring the system defaults.
147    pub fn load_from_name(
148        conn: &Connection,
149        shm: WlShm,
150        name: &str,
151        size: u32,
152    ) -> Result<Self, InvalidId> {
153        // Set some minimal cursor size to hold it. We're not using `size` argument for that,
154        // because the actual size that we'll use depends on theme sizes available on a system.
155        // The minimal size covers most common minimal theme size, which is 16.
156        const INITIAL_POOL_SIZE: i32 = 16 * 16 * 4;
157
158        //  Create shm.
159        let mem_fd = create_shm_fd().expect("Shm fd allocation failed");
160        let mut file = File::from(mem_fd);
161        file.set_len(INITIAL_POOL_SIZE as u64).expect("Failed to set buffer length");
162
163        // Ensure that we have the same we requested.
164        file.write_all(&[0; INITIAL_POOL_SIZE as usize]).expect("Write to shm fd failed");
165        // Flush to ensure the compositor has access to the buffer when it tries to map it.
166        file.flush().expect("Flush on shm fd failed");
167
168        let pool_id = conn.send_request(
169            &shm,
170            wl_shm::Request::CreatePool { fd: file.as_fd(), size: INITIAL_POOL_SIZE },
171            Some(Arc::new(IgnoreObjectData)),
172        )?;
173        let pool = WlShmPool::from_id(conn, pool_id)?;
174
175        let name = String::from(name);
176
177        Ok(Self {
178            name,
179            file,
180            size,
181            pool,
182            pool_size: INITIAL_POOL_SIZE,
183            cursors: Vec::new(),
184            backend: conn.backend().downgrade(),
185            fallback: None,
186        })
187    }
188
189    /// Retrieve a cursor from the theme.
190    ///
191    /// This method returns [`None`] if this cursor is not provided either by the theme, or by one of its parents.
192    ///
193    /// If a [fallback is set], it will use the data returned by the fallback.
194    ///
195    /// [fallback is set]: Self::set_fallback()
196    pub fn get_cursor(&mut self, name: &str) -> Option<&Cursor> {
197        match self.cursors.iter().position(|cursor| cursor.name == name) {
198            Some(i) => Some(&self.cursors[i]),
199            None => {
200                let cursor = match self.load_cursor(name, self.size) {
201                    None => {
202                        let fallback = self.fallback.as_ref()?;
203                        let data = fallback.0(name, self.size)?;
204                        let images = xparser::parse_xcursor(&data)?;
205                        let conn = Connection::from_backend(self.backend.upgrade()?);
206                        Cursor::new(&conn, name, self, &images, self.size)
207                    }
208                    Some(cursor) => cursor,
209                };
210                self.cursors.push(cursor);
211                self.cursors.iter().last()
212            }
213        }
214    }
215
216    /// Set a fallback to load the cursor data, in case the system theme is missing a cursor that you need.
217    ///
218    /// Your fallback will be invoked with the name and size of the requested cursor and should return a byte
219    /// array with the contents of an `xcursor` file, or [`None`] if you don't provide a fallback for this cursor.
220    ///
221    /// For example, this defines a generic fallback cursor image and uses it for all missing cursors:
222    /// ```ignore
223    /// use wayland_cursor::CursorTheme;
224    /// use wayland_client::{Connection, backend::InvalidId, protocol::wl_shm};
225    /// fn example(conn: &Connection, shm: wl_shm::WlShm, size: u32) -> Result<CursorTheme, InvalidId> {
226    ///   let mut theme = CursorTheme::load_or(conn, shm, "default", size)?;
227    ///   theme.set_fallback(|name, size| {
228    ///       include_bytes!("./icons/default")
229    ///   });
230    ///   Ok(theme)
231    /// }
232    /// ```
233    pub fn set_fallback<F>(&mut self, fallback: F)
234    where
235        F: Fn(&str, u32) -> Option<Cow<'static, [u8]>> + Send + Sync + 'static,
236    {
237        self.fallback = Some(FallBack::new(fallback))
238    }
239
240    /// This function loads a cursor, parses it and pushes the images onto the shm pool.
241    ///
242    /// Keep in mind that if the cursor is already loaded, the function will make a duplicate.
243    fn load_cursor(&mut self, name: &str, size: u32) -> Option<Cursor> {
244        let conn = Connection::from_backend(self.backend.upgrade()?);
245        let icon_path = XCursorTheme::load(&self.name).load_icon(name)?;
246        let mut icon_file = File::open(icon_path).ok()?;
247
248        let mut buf = Vec::new();
249        let images = {
250            icon_file.read_to_end(&mut buf).ok()?;
251            xparser::parse_xcursor(&buf)?
252        };
253
254        Some(Cursor::new(&conn, name, self, &images, size))
255    }
256
257    /// Grow the wl_shm_pool this theme is stored on.
258    ///
259    /// This method does nothing if the provided size is smaller or equal to the pool's current size.
260    fn grow(&mut self, size: i32) {
261        if size > self.pool_size {
262            self.file.set_len(size as u64).expect("Failed to set new buffer length");
263            self.pool.resize(size);
264            self.pool_size = size;
265        }
266    }
267}
268
269/// A cursor from a theme. Can contain several images if animated.
270#[derive(Debug, Clone)]
271pub struct Cursor {
272    name: String,
273    images: Vec<CursorImageBuffer>,
274    total_duration: u32,
275}
276
277impl Cursor {
278    /// Construct a new Cursor.
279    ///
280    /// Each of the provided images will be written into `theme`.
281    /// This will also grow `theme.pool` if necessary.
282    fn new(
283        conn: &Connection,
284        name: &str,
285        theme: &mut CursorTheme,
286        images: &[XCursorImage],
287        size: u32,
288    ) -> Self {
289        let mut total_duration = 0;
290        let images: Vec<CursorImageBuffer> = Self::nearest_images(size, images)
291            .map(|image| {
292                let buffer = CursorImageBuffer::new(conn, theme, image);
293                total_duration += buffer.delay;
294
295                buffer
296            })
297            .collect();
298
299        Self { total_duration, name: String::from(name), images }
300    }
301
302    fn nearest_images(size: u32, images: &[XCursorImage]) -> impl Iterator<Item = &XCursorImage> {
303        // Follow the nominal size of the cursor to choose the nearest
304        let nearest_image =
305            images.iter().min_by_key(|image| (size as i32 - image.size as i32).abs()).unwrap();
306
307        images.iter().filter(move |image| {
308            image.width == nearest_image.width && image.height == nearest_image.height
309        })
310    }
311
312    /// Given a time, calculate which frame to show, and how much time remains until the next frame.
313    ///
314    /// Time will wrap, so if for instance the cursor has an animation lasting 100ms,
315    /// then calling this function with 5ms and 105ms as input gives the same output.
316    pub fn frame_and_duration(&self, mut millis: u32) -> FrameAndDuration {
317        millis %= self.total_duration;
318
319        let mut res = 0;
320        for (i, img) in self.images.iter().enumerate() {
321            if millis < img.delay {
322                res = i;
323                break;
324            }
325            millis -= img.delay;
326        }
327
328        FrameAndDuration { frame_index: res, frame_duration: millis }
329    }
330
331    /// Total number of images forming this cursor animation
332    pub fn image_count(&self) -> usize {
333        self.images.len()
334    }
335}
336
337impl Index<usize> for Cursor {
338    type Output = CursorImageBuffer;
339
340    fn index(&self, index: usize) -> &Self::Output {
341        &self.images[index]
342    }
343}
344
345/// A buffer containing a cursor image.
346///
347/// You can access the `WlBuffer` via `Deref`.
348///
349/// Note that this buffer is internally managed by wayland-cursor, as such you should
350/// not try to act on it beyond assigning it to `wl_surface`s.
351#[derive(Debug, Clone)]
352pub struct CursorImageBuffer {
353    buffer: WlBuffer,
354    delay: u32,
355    xhot: u32,
356    yhot: u32,
357    width: u32,
358    height: u32,
359}
360
361impl CursorImageBuffer {
362    /// Construct a new CursorImageBuffer
363    ///
364    /// This function appends the pixels of the image to the provided file,
365    /// and constructs a wl_buffer on that data.
366    fn new(conn: &Connection, theme: &mut CursorTheme, image: &XCursorImage) -> Self {
367        let buf = &image.pixels_rgba;
368        let offset = theme.file.seek(SeekFrom::End(0)).unwrap();
369
370        // Resize memory before writing to it to handle shm correctly.
371        let new_size = offset + buf.len() as u64;
372        theme.grow(new_size as i32);
373
374        theme.file.write_all(buf).unwrap();
375
376        let buffer_id = conn
377            .send_request(
378                &theme.pool,
379                wl_shm_pool::Request::CreateBuffer {
380                    offset: offset as i32,
381                    width: image.width as i32,
382                    height: image.height as i32,
383                    stride: (image.width * 4) as i32,
384                    format: WEnum::Value(Format::Argb8888),
385                },
386                Some(Arc::new(IgnoreObjectData)),
387            )
388            .unwrap();
389
390        let buffer = WlBuffer::from_id(conn, buffer_id).unwrap();
391
392        Self {
393            buffer,
394            delay: image.delay,
395            xhot: image.xhot,
396            yhot: image.yhot,
397            width: image.width,
398            height: image.height,
399        }
400    }
401
402    /// Dimensions of this image
403    pub fn dimensions(&self) -> (u32, u32) {
404        (self.width, self.height)
405    }
406
407    /// Location of the pointer hotspot in this image
408    pub fn hotspot(&self) -> (u32, u32) {
409        (self.xhot, self.yhot)
410    }
411
412    /// Time (in milliseconds) for which this image should be displayed
413    pub fn delay(&self) -> u32 {
414        self.delay
415    }
416}
417
418impl Deref for CursorImageBuffer {
419    type Target = WlBuffer;
420
421    fn deref(&self) -> &WlBuffer {
422        &self.buffer
423    }
424}
425
426/// Which frame to show, and for how long.
427///
428/// This struct is output by `Cursor::frame_and_duration`
429#[derive(Debug, Clone, Eq, PartialEq)]
430pub struct FrameAndDuration {
431    /// The index of the frame which should be shown.
432    pub frame_index: usize,
433    /// The duration that the frame should be shown for (in milliseconds).
434    pub frame_duration: u32,
435}
436
437/// Create a shared file descriptor in memory.
438fn create_shm_fd() -> IoResult<OwnedFd> {
439    // Only try memfd on systems that provide it, (like Linux, Android)
440    #[cfg(any(target_os = "linux", target_os = "android"))]
441    loop {
442        match memfd_create(
443            CStr::from_bytes_with_nul(b"wayland-cursor-rs\0").unwrap(),
444            MemfdFlags::CLOEXEC,
445        ) {
446            Ok(fd) => return Ok(fd),
447            Err(Errno::INTR) => continue,
448            Err(Errno::NOSYS) => break,
449            Err(errno) => return Err(errno.into()),
450        }
451    }
452
453    // Fallback to using shm_open.
454    let sys_time = SystemTime::now();
455    let mut mem_file_handle = format!(
456        "/wayland-cursor-rs-{}",
457        sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
458    );
459    loop {
460        match shm_open(
461            mem_file_handle.as_str(),
462            ShmOFlags::CREATE | ShmOFlags::EXCL | ShmOFlags::RDWR,
463            Mode::RUSR | Mode::WUSR,
464        ) {
465            Ok(fd) => match shm_unlink(mem_file_handle.as_str()) {
466                Ok(_) => return Ok(fd),
467                Err(errno) => return Err(IoError::from(errno)),
468            },
469            Err(Errno::EXIST) => {
470                // If a file with that handle exists then change the handle
471                mem_file_handle = format!(
472                    "/wayland-cursor-rs-{}",
473                    sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
474                );
475                continue;
476            }
477            Err(Errno::INTR) => continue,
478            Err(errno) => return Err(IoError::from(errno)),
479        }
480    }
481}
482
483struct IgnoreObjectData;
484
485impl ObjectData for IgnoreObjectData {
486    fn event(
487        self: Arc<Self>,
488        _: &wayland_client::backend::Backend,
489        _: wayland_client::backend::protocol::Message<wayland_client::backend::ObjectId, OwnedFd>,
490    ) -> Option<Arc<dyn ObjectData>> {
491        None
492    }
493    fn destroyed(&self, _: wayland_client::backend::ObjectId) {}
494}