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