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(data:"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}
495