use std::cell::RefCell; use std::collections::HashMap; use std::env; use std::fs::File; use std::io::Read; use std::rc::Rc; use anyhow::{anyhow, Context}; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::memory::MemoryRenderBuffer; use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData}; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{IsAlive, Logical, Physical, Point, Transform}; use smithay::wayland::compositor::with_states; use xcursor::parser::{parse_xcursor, Image}; use xcursor::CursorTheme; /// Some default looking `left_ptr` icon. static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../resources/cursor.rgba"); type XCursorCache = HashMap<(CursorIcon, i32), Option>>; pub struct CursorManager { theme: CursorTheme, size: u8, current_cursor: CursorImageStatus, named_cursor_cache: RefCell, } impl CursorManager { pub fn new(theme: &str, size: u8) -> Self { Self::ensure_env(theme, size); let theme = CursorTheme::load(theme); Self { theme, size, current_cursor: CursorImageStatus::default_named(), named_cursor_cache: Default::default(), } } /// Reload the cursor theme. pub fn reload(&mut self, theme: &str, size: u8) { Self::ensure_env(theme, size); self.theme = CursorTheme::load(theme); self.size = size; self.named_cursor_cache.get_mut().clear(); } /// Checks if the cursor WlSurface is alive, and if not, cleans it up. pub fn check_cursor_image_surface_alive(&mut self) { if let CursorImageStatus::Surface(surface) = &self.current_cursor { if !surface.alive() { self.current_cursor = CursorImageStatus::default_named(); } } } /// Get the current rendering cursor. pub fn get_render_cursor(&self, scale: i32) -> RenderCursor { match self.current_cursor.clone() { CursorImageStatus::Hidden => RenderCursor::Hidden, CursorImageStatus::Surface(surface) => { let hotspot = with_states(&surface, |states| { states .data_map .get::() .unwrap() .lock() .unwrap() .hotspot }); RenderCursor::Surface { hotspot, surface } } CursorImageStatus::Named(icon) => self.get_render_cursor_named(icon, scale), } } fn get_render_cursor_named(&self, icon: CursorIcon, scale: i32) -> RenderCursor { self.get_cursor_with_name(icon, scale) .map(|cursor| RenderCursor::Named { icon, scale, cursor, }) .unwrap_or_else(|| RenderCursor::Named { icon: Default::default(), scale, cursor: self.get_default_cursor(scale), }) } pub fn is_current_cursor_animated(&self, scale: i32) -> bool { match &self.current_cursor { CursorImageStatus::Hidden => false, CursorImageStatus::Surface(_) => false, CursorImageStatus::Named(icon) => self .get_cursor_with_name(*icon, scale) .unwrap_or_else(|| self.get_default_cursor(scale)) .is_animated_cursor(), } } /// Get named cursor for the given `icon` and `scale`. pub fn get_cursor_with_name(&self, icon: CursorIcon, scale: i32) -> Option> { self.named_cursor_cache .borrow_mut() .entry((icon, scale)) .or_insert_with_key(|(icon, scale)| { let size = self.size as i32 * scale; let mut cursor = Self::load_xcursor(&self.theme, icon.name(), size); // Check alternative names to account for non-compliant themes. if cursor.is_err() { for name in icon.alt_names() { cursor = Self::load_xcursor(&self.theme, name, size); if cursor.is_ok() { break; } } } if let Err(err) = &cursor { warn!("error loading xcursor {}@{size}: {err:?}", icon.name()); } // The default cursor must always have a fallback. if *icon == CursorIcon::Default && cursor.is_err() { cursor = Ok(Self::fallback_cursor()); } cursor.ok().map(Rc::new) }) .clone() } /// Get default cursor. pub fn get_default_cursor(&self, scale: i32) -> Rc { // The default cursor always has a fallback. self.get_cursor_with_name(CursorIcon::Default, scale) .unwrap() } /// Currently used cursor_image as a cursor provider. pub fn cursor_image(&self) -> &CursorImageStatus { &self.current_cursor } /// Set new cursor image provider. pub fn set_cursor_image(&mut self, cursor: CursorImageStatus) { self.current_cursor = cursor; } /// Load the cursor with the given `name` from the file system picking the closest /// one to the given `size`. fn load_xcursor(theme: &CursorTheme, name: &str, size: i32) -> anyhow::Result { let _span = tracy_client::span!("load_xcursor"); let path = theme .load_icon(name) .ok_or_else(|| anyhow!("no default icon"))?; let mut file = File::open(path).context("error opening cursor icon file")?; let mut buf = vec![]; file.read_to_end(&mut buf) .context("error reading cursor icon file")?; let mut images = parse_xcursor(&buf).context("error parsing cursor icon file")?; let (width, height) = images .iter() .min_by_key(|image| (size - image.size as i32).abs()) .map(|image| (image.width, image.height)) .unwrap(); images.retain(move |image| image.width == width && image.height == height); let animation_duration = images.iter().fold(0, |acc, image| acc + image.delay); Ok(XCursor { images, animation_duration, }) } /// Set the common XCURSOR env variables. fn ensure_env(theme: &str, size: u8) { env::set_var("XCURSOR_THEME", theme); env::set_var("XCURSOR_SIZE", size.to_string()); } fn fallback_cursor() -> XCursor { let images = vec![Image { size: 32, width: 64, height: 64, xhot: 1, yhot: 1, delay: 0, pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), pixels_argb: vec![], }]; XCursor { images, animation_duration: 0, } } } /// The cursor prepared for renderer. pub enum RenderCursor { Hidden, Surface { hotspot: Point, surface: WlSurface, }, Named { icon: CursorIcon, scale: i32, cursor: Rc, }, } type TextureCache = HashMap<(CursorIcon, i32), Vec>; #[derive(Default)] pub struct CursorTextureCache { cache: RefCell, } impl CursorTextureCache { pub fn clear(&mut self) { self.cache.get_mut().clear(); } pub fn get( &self, icon: CursorIcon, scale: i32, cursor: &XCursor, idx: usize, ) -> MemoryRenderBuffer { self.cache .borrow_mut() .entry((icon, scale)) .or_insert_with(|| { cursor .frames() .iter() .map(|frame| { MemoryRenderBuffer::from_slice( &frame.pixels_rgba, Fourcc::Argb8888, (frame.width as i32, frame.height as i32), scale, Transform::Normal, None, ) }) .collect() })[idx] .clone() } } // The XCursorBuffer implementation is inspired by `wayland-rs`, thus provided under MIT license. /// The state of the `NamedCursor`. pub struct XCursor { /// The image for the underlying named cursor. images: Vec, /// The total duration of the animation. animation_duration: u32, } impl XCursor { /// Given a time, calculate which frame to show, and how much time remains until the next frame. /// /// Time will wrap, so if for instance the cursor has an animation lasting 100ms, /// then calling this function with 5ms and 105ms as input gives the same output. pub fn frame(&self, mut millis: u32) -> (usize, &Image) { if self.animation_duration == 0 { return (0, &self.images[0]); } millis %= self.animation_duration; let mut res = 0; for (i, img) in self.images.iter().enumerate() { if millis < img.delay { res = i; break; } millis -= img.delay; } (res, &self.images[res]) } /// Get the frames for the given `XCursor`. pub fn frames(&self) -> &[Image] { &self.images } /// Check whether the cursor is animated. pub fn is_animated_cursor(&self) -> bool { self.images.len() > 1 } /// Get hotspot for the given `image`. pub fn hotspot(image: &Image) -> Point { (image.xhot as i32, image.yhot as i32).into() } }