Files
niri/src/cursor.rs
T
Kirill Chibisov 6a81d8f2a6 Check alternative cursor names when loading cursor
Some themes don't have all the cursors specified as in w3c
specification, thus try to check for alternative names as well.
2023-11-02 16:23:05 +04:00

324 lines
10 KiB
Rust

use std::cell::RefCell;
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::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 XCursorCache = HashMap<(CursorIcon, i32), Option<Rc<XCursor>>>;
pub struct CursorManager {
theme: CursorTheme,
size: u8,
current_cursor: CursorImageStatus,
named_cursor_cache: RefCell<XCursorCache>,
}
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);
// 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<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());
}
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<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,
cursor: &XCursor,
idx: usize,
) -> TextureBuffer<GlesTexture> {
self.cache
.borrow_mut()
.entry((icon, scale))
.or_insert_with(|| {
cursor
.frames()
.iter()
.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()
})
.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<Image>,
/// 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<i32, Physical> {
(image.xhot as i32, image.yhot as i32).into()
}
}