#![warn(missing_docs, missing_debug_implementations)]
#![forbid(improper_ctypes, unsafe_op_in_unsafe_fn)]
use std::borrow::Cow;
use std::env;
use std::fmt::Debug;
use std::fs::File;
use std::io::{Error as IoError, Read, Result as IoResult, Seek, SeekFrom, Write};
use std::ops::{Deref, Index};
use std::os::unix::io::{AsFd, OwnedFd};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rustix::fs::Mode;
#[cfg(any(target_os = "linux", target_os = "android"))]
use rustix::fs::{memfd_create, MemfdFlags};
use rustix::io::Errno;
use rustix::shm::{shm_open, shm_unlink, ShmOFlags};
#[cfg(any(target_os = "linux", target_os = "android"))]
use std::ffi::CStr;
use wayland_client::backend::{InvalidId, ObjectData, WeakBackend};
use wayland_client::protocol::wl_buffer::WlBuffer;
use wayland_client::protocol::wl_shm::{self, Format, WlShm};
use wayland_client::protocol::wl_shm_pool::{self, WlShmPool};
use wayland_client::{Connection, Proxy, WEnum};
use xcursor::parser as xparser;
use xcursor::CursorTheme as XCursorTheme;
use xparser::Image as XCursorImage;
#[derive(Debug)]
pub struct CursorTheme {
name: String,
cursors: Vec<Cursor>,
size: u32,
pool: WlShmPool,
pool_size: i32,
file: File,
backend: WeakBackend,
fallback: Option<FallBack>,
}
type FallBackInner = Box<dyn Fn(&str, u32) -> Option<Cow<'static, [u8]>> + Send + Sync>;
struct FallBack(FallBackInner);
impl FallBack {
fn new<F>(fallback: F) -> Self
where
F: Fn(&str, u32) -> Option<Cow<'static, [u8]>> + Send + Sync + 'static,
{
Self(Box::new(fallback))
}
}
impl Debug for FallBack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("fallback function")
}
}
impl CursorTheme {
pub fn load(conn: &Connection, shm: WlShm, size: u32) -> Result<Self, InvalidId> {
Self::load_or(conn, shm, "default", size)
}
pub fn load_or(
conn: &Connection,
shm: WlShm,
name: &str,
mut size: u32,
) -> Result<Self, InvalidId> {
let name_string = String::from(name);
let name = &env::var("XCURSOR_THEME").unwrap_or(name_string);
if let Ok(var) = env::var("XCURSOR_SIZE") {
if let Ok(int) = var.parse() {
size = int;
}
}
Self::load_from_name(conn, shm, name, size)
}
pub fn load_from_name(
conn: &Connection,
shm: WlShm,
name: &str,
size: u32,
) -> Result<Self, InvalidId> {
const INITIAL_POOL_SIZE: i32 = 16 * 16 * 4;
let mem_fd = create_shm_fd().expect("Shm fd allocation failed");
let mut file = File::from(mem_fd);
file.set_len(INITIAL_POOL_SIZE as u64).expect("Failed to set buffer length");
file.write_all(&[0; INITIAL_POOL_SIZE as usize]).expect("Write to shm fd failed");
file.flush().expect("Flush on shm fd failed");
let pool_id = conn.send_request(
&shm,
wl_shm::Request::CreatePool { fd: file.as_fd(), size: INITIAL_POOL_SIZE },
Some(Arc::new(IgnoreObjectData)),
)?;
let pool = WlShmPool::from_id(conn, pool_id)?;
let name = String::from(name);
Ok(Self {
name,
file,
size,
pool,
pool_size: INITIAL_POOL_SIZE,
cursors: Vec::new(),
backend: conn.backend().downgrade(),
fallback: None,
})
}
pub fn get_cursor(&mut self, name: &str) -> Option<&Cursor> {
match self.cursors.iter().position(|cursor| cursor.name == name) {
Some(i) => Some(&self.cursors[i]),
None => {
let cursor = match self.load_cursor(name, self.size) {
None => {
let fallback = self.fallback.as_ref()?;
let data = fallback.0(name, self.size)?;
let images = xparser::parse_xcursor(&data)?;
let conn = Connection::from_backend(self.backend.upgrade()?);
Cursor::new(&conn, name, self, &images, self.size)
}
Some(cursor) => cursor,
};
self.cursors.push(cursor);
self.cursors.iter().last()
}
}
}
pub fn set_fallback<F>(&mut self, fallback: F)
where
F: Fn(&str, u32) -> Option<Cow<'static, [u8]>> + Send + Sync + 'static,
{
self.fallback = Some(FallBack::new(fallback))
}
fn load_cursor(&mut self, name: &str, size: u32) -> Option<Cursor> {
let conn = Connection::from_backend(self.backend.upgrade()?);
let icon_path = XCursorTheme::load(&self.name).load_icon(name)?;
let mut icon_file = File::open(icon_path).ok()?;
let mut buf = Vec::new();
let images = {
icon_file.read_to_end(&mut buf).ok()?;
xparser::parse_xcursor(&buf)?
};
Some(Cursor::new(&conn, name, self, &images, size))
}
fn grow(&mut self, size: i32) {
if size > self.pool_size {
self.file.set_len(size as u64).expect("Failed to set new buffer length");
self.pool.resize(size);
self.pool_size = size;
}
}
}
#[derive(Debug, Clone)]
pub struct Cursor {
name: String,
images: Vec<CursorImageBuffer>,
total_duration: u32,
}
impl Cursor {
fn new(
conn: &Connection,
name: &str,
theme: &mut CursorTheme,
images: &[XCursorImage],
size: u32,
) -> Self {
let mut total_duration = 0;
let images: Vec<CursorImageBuffer> = Self::nearest_images(size, images)
.map(|image| {
let buffer = CursorImageBuffer::new(conn, theme, image);
total_duration += buffer.delay;
buffer
})
.collect();
Self { total_duration, name: String::from(name), images }
}
fn nearest_images(size: u32, images: &[XCursorImage]) -> impl Iterator<Item = &XCursorImage> {
let nearest_image =
images.iter().min_by_key(|image| (size as i32 - image.size as i32).abs()).unwrap();
images.iter().filter(move |image| {
image.width == nearest_image.width && image.height == nearest_image.height
})
}
pub fn frame_and_duration(&self, mut millis: u32) -> FrameAndDuration {
millis %= self.total_duration;
let mut res = 0;
for (i, img) in self.images.iter().enumerate() {
if millis < img.delay {
res = i;
break;
}
millis -= img.delay;
}
FrameAndDuration { frame_index: res, frame_duration: millis }
}
pub fn image_count(&self) -> usize {
self.images.len()
}
}
impl Index<usize> for Cursor {
type Output = CursorImageBuffer;
fn index(&self, index: usize) -> &Self::Output {
&self.images[index]
}
}
#[derive(Debug, Clone)]
pub struct CursorImageBuffer {
buffer: WlBuffer,
delay: u32,
xhot: u32,
yhot: u32,
width: u32,
height: u32,
}
impl CursorImageBuffer {
fn new(conn: &Connection, theme: &mut CursorTheme, image: &XCursorImage) -> Self {
let buf = &image.pixels_rgba;
let offset = theme.file.seek(SeekFrom::End(0)).unwrap();
let new_size = offset + buf.len() as u64;
theme.grow(new_size as i32);
theme.file.write_all(buf).unwrap();
let buffer_id = conn
.send_request(
&theme.pool,
wl_shm_pool::Request::CreateBuffer {
offset: offset as i32,
width: image.width as i32,
height: image.height as i32,
stride: (image.width * 4) as i32,
format: WEnum::Value(Format::Argb8888),
},
Some(Arc::new(IgnoreObjectData)),
)
.unwrap();
let buffer = WlBuffer::from_id(conn, buffer_id).unwrap();
Self {
buffer,
delay: image.delay,
xhot: image.xhot,
yhot: image.yhot,
width: image.width,
height: image.height,
}
}
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn hotspot(&self) -> (u32, u32) {
(self.xhot, self.yhot)
}
pub fn delay(&self) -> u32 {
self.delay
}
}
impl Deref for CursorImageBuffer {
type Target = WlBuffer;
fn deref(&self) -> &WlBuffer {
&self.buffer
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct FrameAndDuration {
pub frame_index: usize,
pub frame_duration: u32,
}
fn create_shm_fd() -> IoResult<OwnedFd> {
#[cfg(any(target_os = "linux", target_os = "android"))]
loop {
match memfd_create(
CStr::from_bytes_with_nul(b"wayland-cursor-rs\0").unwrap(),
MemfdFlags::CLOEXEC,
) {
Ok(fd) => return Ok(fd),
Err(Errno::INTR) => continue,
Err(Errno::NOSYS) => break,
Err(errno) => return Err(errno.into()),
}
}
let sys_time = SystemTime::now();
let mut mem_file_handle = format!(
"/wayland-cursor-rs-{}",
sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
);
loop {
match shm_open(
mem_file_handle.as_str(),
ShmOFlags::CREATE | ShmOFlags::EXCL | ShmOFlags::RDWR,
Mode::RUSR | Mode::WUSR,
) {
Ok(fd) => match shm_unlink(mem_file_handle.as_str()) {
Ok(_) => return Ok(fd),
Err(errno) => return Err(IoError::from(errno)),
},
Err(Errno::EXIST) => {
mem_file_handle = format!(
"/wayland-cursor-rs-{}",
sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
);
continue;
}
Err(Errno::INTR) => continue,
Err(errno) => return Err(IoError::from(errno)),
}
}
}
struct IgnoreObjectData;
impl ObjectData for IgnoreObjectData {
fn event(
self: Arc<Self>,
_: &wayland_client::backend::Backend,
_: wayland_client::backend::protocol::Message<wayland_client::backend::ObjectId, OwnedFd>,
) -> Option<Arc<dyn ObjectData>> {
None
}
fn destroyed(&self, _: wayland_client::backend::ObjectId) {}
}