diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2023-10-29 14:04:38 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2023-10-30 14:00:27 +0400 |
| commit | 088877889d738e7d539243a239441bd82eb44a2c (patch) | |
| tree | 59f189318ef70af0e91e4d061543ff7840e8b6ae /src/cursor.rs | |
| parent | 5c24754435b6681f74800fb5a91444a9c5e5ac78 (diff) | |
| download | niri-088877889d738e7d539243a239441bd82eb44a2c.tar.gz niri-088877889d738e7d539243a239441bd82eb44a2c.tar.bz2 niri-088877889d738e7d539243a239441bd82eb44a2c.zip | |
Add cursor-shape protocol
Diffstat (limited to 'src/cursor.rs')
| -rw-r--r-- | src/cursor.rs | 346 |
1 files changed, 270 insertions, 76 deletions
diff --git a/src/cursor.rs b/src/cursor.rs index 46d2b689..bef62b51 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -3,116 +3,310 @@ use std::collections::HashMap; use std::env; use std::fs::File; use std::io::Read; +use std::rc::Rc; +use std::sync::Mutex; use anyhow::{anyhow, Context}; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::texture::TextureBuffer; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; -use smithay::utils::{Physical, Point, Transform}; +use smithay::input::pointer::{CursorIcon, CursorImageAttributes, CursorImageStatus}; +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 CursorCache = HashMap<i32, (TextureBuffer<GlesTexture>, Point<i32, Physical>)>; +type XCursorCache = HashMap<(CursorIcon, i32), Option<Rc<XCursor>>>; -pub struct Cursor { - images: Vec<Image>, - size: i32, - cache: RefCell<CursorCache>, +pub struct CursorManager { + theme: CursorTheme, + size: u8, + current_cursor: CursorImageStatus, + named_cursor_cache: RefCell<XCursorCache>, } -impl Cursor { - /// Load the said theme as well as set the `XCURSOR_THEME` and `XCURSOR_SIZE` - /// env variables. - pub fn load(theme: &str, size: u8) -> Self { +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::<Mutex<CursorImageAttributes>>() + .unwrap() + .lock() + .unwrap() + .hotspot + }); + + RenderCursor::Surface { hotspot, surface } + } + CursorImageStatus::Named(icon) => 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<Rc<XCursor>> { + 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); + 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<XCursor> { + // The default cursor always has a fallback. + self.get_cursor_with_name(CursorIcon::Default, scale) + .unwrap() + } + + /// Currenly 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<XCursor> { + 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()); + } - let images = match load_xcursor(theme) { - Ok(images) => images, - Err(err) => { - warn!("error loading xcursor default cursor: {err:?}"); - - vec![Image { - size: 32, - width: 64, - height: 64, - xhot: 1, - yhot: 1, - delay: 1, - pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), - pixels_argb: vec![], - }] - } - }; + 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![], + }]; - Self { + XCursor { images, - size: size as i32, - cache: Default::default(), + animation_duration: 0, } } +} + +/// The cursor prepared for renderer. +pub enum RenderCursor { + Hidden, + Surface { + hotspot: Point<i32, Logical>, + surface: WlSurface, + }, + Named { + icon: CursorIcon, + scale: i32, + cursor: Rc<XCursor>, + }, +} + +type TextureCache = HashMap<(CursorIcon, i32), Vec<TextureBuffer<GlesTexture>>>; + +#[derive(Default)] +pub struct CursorTextureCache { + cache: RefCell<TextureCache>, +} + +impl CursorTextureCache { + pub fn clear(&mut self) { + self.cache.get_mut().clear(); + } pub fn get( &self, renderer: &mut GlesRenderer, + icon: CursorIcon, scale: i32, - ) -> (TextureBuffer<GlesTexture>, Point<i32, Physical>) { + cursor: &XCursor, + idx: usize, + ) -> TextureBuffer<GlesTexture> { self.cache .borrow_mut() - .entry(scale) - .or_insert_with_key(|scale| { - let _span = tracy_client::span!("create cursor texture"); - - let size = self.size * scale; - - let nearest_image = self - .images - .iter() - .min_by_key(|image| (size - image.size as i32).abs()) - .unwrap(); - let frame = self - .images + .entry((icon, scale)) + .or_insert_with(|| { + cursor + .frames() .iter() - .find(move |image| { - image.width == nearest_image.width && image.height == nearest_image.height + .map(|frame| { + let _span = tracy_client::span!("create TextureBuffer"); + + TextureBuffer::from_memory( + renderer, + &frame.pixels_rgba, + Fourcc::Abgr8888, + (frame.width as i32, frame.height as i32), + false, + scale, + Transform::Normal, + None, + ) + .unwrap() }) - .unwrap(); - - let texture = TextureBuffer::from_memory( - renderer, - &frame.pixels_rgba, - Fourcc::Abgr8888, - (frame.width as i32, frame.height as i32), - false, - *scale, - Transform::Normal, - None, - ) - .unwrap(); - (texture, (frame.xhot as i32, frame.yhot as i32).into()) - }) + .collect() + })[idx] .clone() } +} - pub fn get_cached_hotspot(&self, scale: i32) -> Option<Point<i32, Physical>> { - self.cache.borrow().get(&scale).map(|(_, hotspot)| *hotspot) - } +// 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<Image>, + /// The total duration of the animation. + animation_duration: u32, } -fn load_xcursor(theme: &str) -> anyhow::Result<Vec<Image>> { - let _span = tracy_client::span!(); +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]); + } - let theme = CursorTheme::load(theme); - let path = theme - .load_icon("default") - .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 images = parse_xcursor(&buf).context("error parsing cursor icon file")?; + millis %= self.animation_duration; - Ok(images) + 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<i32, Physical> { + (image.xhot as i32, image.yhot as i32).into() + } } |
