| 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 | |
| 48 | use std::borrow::Cow; |
| 49 | use std::env; |
| 50 | use std::fmt::Debug; |
| 51 | use std::fs::File; |
| 52 | use std::io::{Error as IoError, Read, Result as IoResult, Seek, SeekFrom, Write}; |
| 53 | use std::ops::{Deref, Index}; |
| 54 | use std::os::unix::io::{AsFd, OwnedFd}; |
| 55 | use std::sync::Arc; |
| 56 | use std::time::{SystemTime, UNIX_EPOCH}; |
| 57 | |
| 58 | use rustix::fs::Mode; |
| 59 | #[cfg (any(target_os = "linux" , target_os = "android" ))] |
| 60 | use rustix::fs::{memfd_create, MemfdFlags}; |
| 61 | use rustix::io::Errno; |
| 62 | use rustix::shm::{shm_open, shm_unlink, ShmOFlags}; |
| 63 | #[cfg (any(target_os = "linux" , target_os = "android" ))] |
| 64 | use std::ffi::CStr; |
| 65 | |
| 66 | use wayland_client::backend::{InvalidId, ObjectData, WeakBackend}; |
| 67 | use wayland_client::protocol::wl_buffer::WlBuffer; |
| 68 | use wayland_client::protocol::wl_shm::{self, Format, WlShm}; |
| 69 | use wayland_client::protocol::wl_shm_pool::{self, WlShmPool}; |
| 70 | use wayland_client::{Connection, Proxy, WEnum}; |
| 71 | |
| 72 | use xcursor::parser as xparser; |
| 73 | use xcursor::CursorTheme as XCursorTheme; |
| 74 | use xparser::Image as XCursorImage; |
| 75 | |
| 76 | /// Represents a cursor theme loaded from the system. |
| 77 | #[derive (Debug)] |
| 78 | pub 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 | |
| 89 | type FallBackInner = Box<dyn Fn(&str, u32) -> Option<Cow<'static, [u8]>> + Send + Sync>; |
| 90 | |
| 91 | struct FallBack(FallBackInner); |
| 92 | |
| 93 | impl 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 | |
| 102 | impl 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 | |
| 108 | impl 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)] |
| 271 | pub struct Cursor { |
| 272 | name: String, |
| 273 | images: Vec<CursorImageBuffer>, |
| 274 | total_duration: u32, |
| 275 | } |
| 276 | |
| 277 | impl 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 | |
| 337 | impl 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)] |
| 352 | pub struct CursorImageBuffer { |
| 353 | buffer: WlBuffer, |
| 354 | delay: u32, |
| 355 | xhot: u32, |
| 356 | yhot: u32, |
| 357 | width: u32, |
| 358 | height: u32, |
| 359 | } |
| 360 | |
| 361 | impl 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 | |
| 418 | impl 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)] |
| 430 | pub 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. |
| 438 | fn 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 | |
| 483 | struct IgnoreObjectData; |
| 484 | |
| 485 | impl 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 | |