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::env; |
49 | use std::fs::File; |
50 | use std::io::{Error as IoError, Read, Result as IoResult, Seek, SeekFrom, Write}; |
51 | use std::ops::{Deref, Index}; |
52 | use std::os::unix::io::{AsFd, OwnedFd}; |
53 | use std::sync::Arc; |
54 | use std::time::{SystemTime, UNIX_EPOCH}; |
55 | |
56 | use rustix::fs::Mode; |
57 | #[cfg (any(target_os = "linux" , target_os = "android" ))] |
58 | use rustix::fs::{memfd_create, MemfdFlags}; |
59 | use rustix::io::Errno; |
60 | use rustix::shm::{shm_open, shm_unlink, ShmOFlags}; |
61 | #[cfg (any(target_os = "linux" , target_os = "android" ))] |
62 | use std::ffi::CStr; |
63 | |
64 | use wayland_client::backend::{InvalidId, ObjectData, WeakBackend}; |
65 | use wayland_client::protocol::wl_buffer::WlBuffer; |
66 | use wayland_client::protocol::wl_shm::{self, Format, WlShm}; |
67 | use wayland_client::protocol::wl_shm_pool::{self, WlShmPool}; |
68 | use wayland_client::{Connection, Proxy, WEnum}; |
69 | |
70 | use xcursor::parser as xparser; |
71 | use xcursor::CursorTheme as XCursorTheme; |
72 | use xparser::Image as XCursorImage; |
73 | |
74 | /// Represents a cursor theme loaded from the system. |
75 | #[derive (Debug)] |
76 | pub 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 | |
86 | impl 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)] |
211 | pub struct Cursor { |
212 | name: String, |
213 | images: Vec<CursorImageBuffer>, |
214 | total_duration: u32, |
215 | } |
216 | |
217 | impl 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 | |
277 | impl 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)] |
292 | pub struct CursorImageBuffer { |
293 | buffer: WlBuffer, |
294 | delay: u32, |
295 | xhot: u32, |
296 | yhot: u32, |
297 | width: u32, |
298 | height: u32, |
299 | } |
300 | |
301 | impl 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 | |
358 | impl 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)] |
370 | pub 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. |
378 | fn 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 | |
423 | struct IgnoreObjectData; |
424 | |
425 | impl 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 | |