1use waybackend::{Waybackend, types::ObjectId};
13
14#[derive(Debug)]
15pub enum CursorLoadError {
16 Io(rustix::io::Errno),
17 EnvVar(std::env::VarError),
18}
19
20impl std::error::Error for CursorLoadError {
21 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
22 match self {
23 CursorLoadError::Io(errno) => errno.source(),
24 CursorLoadError::EnvVar(var_error) => var_error.source(),
25 }
26 }
27
28 fn cause(&self) -> Option<&dyn std::error::Error> {
29 self.source()
30 }
31}
32
33impl std::fmt::Display for CursorLoadError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "CursorLoadError: ")?;
36 match self {
37 CursorLoadError::Io(errno) => errno.fmt(f),
38 CursorLoadError::EnvVar(var_error) => var_error.fmt(f),
39 }
40 }
41}
42
43pub struct CursorTheme {
44 name: String,
45 cursors: Vec<Cursor>,
46 size: u32,
48 wl_shm_pool: ObjectId,
49 pool_size: u32,
50 pool_len: u32,
51 shm_fd: rustix::fd::OwnedFd,
52}
53
54impl CursorTheme {
55 const INITIAL_POOL_SIZE: usize = 16 * 16 * 4;
57
58 #[inline]
62 pub fn load_from_env<F>(mut size: u32, create_pool_fn: F) -> Result<Self, CursorLoadError>
63 where
64 F: FnOnce(&rustix::fd::OwnedFd, u32) -> ObjectId,
65 {
66 let name = &std::env::var("XCURSOR_THEME").map_err(CursorLoadError::EnvVar)?;
67
68 if let Ok(var) = std::env::var("XCURSOR_SIZE")
69 && let Ok(int) = var.parse()
70 {
71 size = int;
72 }
73
74 Self::load_from_name(name, size, create_pool_fn)
75 }
76
77 #[inline]
116 pub fn load_from_name<F>(
117 name: &str,
118 size: u32,
119 create_pool_fn: F,
120 ) -> Result<Self, CursorLoadError>
121 where
122 F: FnOnce(&rustix::fd::OwnedFd, u32) -> ObjectId,
123 {
124 let shm_fd = waybackend::shm::create().map_err(CursorLoadError::Io)?;
126 rustix::fs::ftruncate(&shm_fd, Self::INITIAL_POOL_SIZE as u64)
127 .map_err(CursorLoadError::Io)?;
128 let wl_shm_pool = create_pool_fn(&shm_fd, size);
129
130 let name = String::from(name);
131 Ok(Self {
132 name,
133 size,
134 pool_size: Self::INITIAL_POOL_SIZE as u32,
135 pool_len: 0,
136 cursors: Vec::new(),
137 wl_shm_pool,
138 shm_fd,
139 })
140 }
141
142 #[inline]
195 pub fn get_cursor<F1, F2>(
196 &mut self,
197 name: &str,
198 backend: &mut Waybackend,
199 create_buffer_fn: F1,
200 resize_fn: F2,
201 ) -> Option<&Cursor>
202 where
203 F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
204 F2: FnMut(&mut Waybackend, ObjectId, u32),
205 {
206 match self.cursors.iter().position(|cursor| cursor.name == name) {
207 Some(i) => Some(&self.cursors[i]),
208 None => {
209 let cursor =
210 self.load_cursor(name, self.size, backend, create_buffer_fn, resize_fn)?;
211 self.cursors.push(cursor);
212 self.cursors.iter().last()
213 }
214 }
215 }
216
217 #[inline]
218 fn load_cursor<F1, F2>(
219 &mut self,
220 name: &str,
221 size: u32,
222 backend: &mut Waybackend,
223 create_buffer_fn: F1,
224 resize_fn: F2,
225 ) -> Option<Cursor>
226 where
227 F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
228 F2: FnMut(&mut Waybackend, ObjectId, u32),
229 {
230 let icon_path = xcursor::CursorTheme::load(&self.name).load_icon(name)?;
231 let mut icon_file = std::fs::File::open(icon_path).ok()?;
232
233 let mut buf = Vec::new();
234 let images = {
235 std::io::Read::read_to_end(&mut icon_file, &mut buf).ok()?;
236 xcursor::parser::parse_xcursor(&buf)?
237 };
238
239 Some(Cursor::new(
240 name,
241 self,
242 &images,
243 size,
244 backend,
245 create_buffer_fn,
246 resize_fn,
247 ))
248 }
249
250 #[inline]
251 fn grow<F>(&mut self, backend: &mut Waybackend, size: u32, resize_fn: F)
252 where
253 F: FnOnce(&mut Waybackend, ObjectId, u32),
254 {
255 if size > self.pool_size {
256 rustix::fs::ftruncate(&self.shm_fd, size as u64)
257 .expect("failed to new cursor buffer length");
258 resize_fn(backend, self.wl_shm_pool, size);
259 self.pool_size = size;
260 }
261 }
262}
263
264pub struct Cursor {
265 name: String,
266 images: Vec<CursorImageBuffer>,
267 total_duration: u32,
268}
269
270impl Cursor {
271 #[inline]
272 fn new<F1, F2>(
273 name: &str,
274 theme: &mut CursorTheme,
275 images: &[xcursor::parser::Image],
276 size: u32,
277 backend: &mut Waybackend,
278 mut create_buffer_fn: F1,
279 mut resize_fn: F2,
280 ) -> Self
281 where
282 F1: FnMut(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
283 F2: FnMut(&mut Waybackend, ObjectId, u32),
284 {
285 let mut total_duration = 0;
286 let f1 = &mut create_buffer_fn;
287 let f2 = &mut resize_fn;
288 let images: Vec<CursorImageBuffer> = Self::nearest_images(size, images)
289 .map(|image| {
290 let buffer =
291 CursorImageBuffer::new::<&mut F1, &mut F2>(theme, image, backend, f1, f2);
292 total_duration += buffer.delay;
293
294 buffer
295 })
296 .collect();
297
298 Self {
299 total_duration,
300 name: String::from(name),
301 images,
302 }
303 }
304
305 #[inline]
306 fn nearest_images(
307 size: u32,
308 images: &[xcursor::parser::Image],
309 ) -> impl Iterator<Item = &xcursor::parser::Image> {
310 let nearest_image = images
312 .iter()
313 .min_by_key(|image| (size as i32 - image.size as i32).abs())
314 .unwrap();
315 images.iter().filter(move |image| {
316 image.width == nearest_image.width && image.height == nearest_image.height
317 })
318 }
319
320 #[inline]
322 pub fn frame_and_duration(&self, mut millis: u32) -> (usize, u32) {
323 millis %= self.total_duration;
324 let mut res = 0;
325 for (i, img) in self.images.iter().enumerate() {
326 if millis < img.delay {
327 res = i;
328 break;
329 }
330 millis -= img.delay;
331 }
332
333 (res, millis)
334 }
335
336 #[inline]
337 pub fn images(&self) -> &[CursorImageBuffer] {
338 &self.images
339 }
340}
341
342pub struct CursorImageBuffer {
343 wl_buffer: ObjectId,
344 delay: u32,
345 xhot: u32,
346 yhot: u32,
347 width: u32,
348 height: u32,
349}
350
351impl CursorImageBuffer {
352 #[inline]
357 fn new<F1, F2>(
358 theme: &mut CursorTheme,
359 image: &xcursor::parser::Image,
360 backend: &mut Waybackend,
361 create_buffer_fn: F1,
362 resize_fn: F2,
363 ) -> Self
364 where
365 F1: FnOnce(&mut Waybackend, ObjectId, i32, i32, i32, i32) -> ObjectId,
366 F2: FnOnce(&mut Waybackend, ObjectId, u32),
367 {
368 use rustix::mm::{MapFlags, ProtFlags, mmap, munmap};
369 let buf = &image.pixels_rgba;
370 let offset = theme.pool_len as u64;
371 let new_size = offset + buf.len() as u64;
373 theme.grow(backend, new_size as u32, resize_fn);
374 let mmap = unsafe {
375 mmap(
376 std::ptr::null_mut(),
377 new_size as usize,
378 ProtFlags::READ | ProtFlags::WRITE,
379 MapFlags::SHARED,
380 &theme.shm_fd,
381 0,
382 )
383 }
384 .expect("failed to mmap cursor shared memory");
385 {
386 let slice = unsafe {
387 std::slice::from_raw_parts_mut(mmap.cast::<u8>().add(offset as usize), buf.len())
388 };
389 slice.copy_from_slice(buf);
390 }
391 theme.pool_len += buf.len() as u32;
392
393 let wl_buffer = create_buffer_fn(
394 backend,
395 theme.wl_shm_pool,
396 offset as i32,
397 image.width as i32,
398 image.height as i32,
399 (image.width * 4) as i32,
400 );
401
402 unsafe { munmap(mmap, new_size as usize).expect("failed to munmap cursor shared memory") };
403 Self {
404 wl_buffer,
405 delay: image.delay,
406 xhot: image.xhot,
407 yhot: image.yhot,
408 width: image.width,
409 height: image.height,
410 }
411 }
412
413 #[inline]
415 pub fn dimensions(&self) -> (u32, u32) {
416 (self.width, self.height)
417 }
418
419 #[inline]
421 pub fn hotspot(&self) -> (u32, u32) {
422 (self.xhot, self.yhot)
423 }
424
425 #[inline]
427 pub fn delay(&self) -> u32 {
428 self.delay
429 }
430
431 #[inline]
433 pub fn wl_buffer(&self) -> ObjectId {
434 self.wl_buffer
435 }
436}