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