use { crate::{ format::ARGB8888, rect::Rect, render::{RenderContext, RenderError, Renderer, Texture}, utils::{errorfmt::ErrorFmt, numcell::NumCell}, }, ahash::AHashSet, bstr::{BStr, BString, ByteSlice, ByteVec}, byteorder::{LittleEndian, ReadBytesExt}, isnt::std_1::primitive::IsntSliceExt, std::{ cell::Cell, convert::TryInto, env, fmt::{Debug, Formatter}, fs::File, io::{self, BufRead, BufReader, Seek, SeekFrom}, mem::MaybeUninit, rc::Rc, slice, str, }, thiserror::Error, uapi::c, }; const XCURSOR_MAGIC: u32 = 0x72756358; const XCURSOR_IMAGE_TYPE: u32 = 0xfffd0002; const XCURSOR_PATH_DEFAULT: &[u8] = b"~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons"; const XCURSOR_PATH: &str = "XCURSOR_PATH"; const HOME: &str = "HOME"; const HEADER_SIZE: u32 = 16; pub trait Cursor { fn set_position(&self, x: i32, y: i32); fn render(&self, renderer: &mut Renderer, x: i32, y: i32); fn get_hotspot(&self) -> (i32, i32); fn extents(&self) -> Rect; fn handle_unset(&self) {} fn tick(&self) {} } pub struct ServerCursors { pub default: ServerCursorTemplate, pub resize_right: ServerCursorTemplate, pub resize_left: ServerCursorTemplate, pub resize_top: ServerCursorTemplate, pub resize_bottom: ServerCursorTemplate, pub resize_top_bottom: ServerCursorTemplate, pub resize_left_right: ServerCursorTemplate, pub resize_top_left: ServerCursorTemplate, pub resize_top_right: ServerCursorTemplate, pub resize_bottom_left: ServerCursorTemplate, pub resize_bottom_right: ServerCursorTemplate, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum KnownCursor { Default, ResizeLeftRight, ResizeTopBottom, ResizeTopLeft, ResizeTopRight, ResizeBottomLeft, ResizeBottomRight, } impl ServerCursors { pub fn load(ctx: &Rc) -> Result { let paths = find_cursor_paths(); log::debug!("Trying to load cursors from paths {:?}", paths); let load = |name: &str| ServerCursorTemplate::load(name, None, 16, &paths, ctx); Ok(Self { default: load("left_ptr")?, // default: load("left_ptr_watch")?, resize_right: load("right_side")?, resize_left: load("left_side")?, resize_top: load("top_side")?, resize_bottom: load("bottom_side")?, resize_top_bottom: load("v_double_arrow")?, resize_left_right: load("h_double_arrow")?, resize_top_left: load("top_left_corner")?, resize_top_right: load("top_right_corner")?, resize_bottom_left: load("bottom_left_corner")?, resize_bottom_right: load("bottom_right_corner")?, }) } } pub struct ServerCursorTemplate { var: ServerCursorTemplateVariant, pub xcursor: Vec, } enum ServerCursorTemplateVariant { Static(Rc), Animated(Rc>), } impl ServerCursorTemplate { fn load( name: &str, theme: Option<&BStr>, size: u32, paths: &[BString], ctx: &Rc, ) -> Result { match open_cursor(name, theme, size, paths) { Ok(cs) => { if cs.len() == 1 { let c = &cs[0]; let cursor = CursorImage::from_bytes( ctx, &c.pixels, 0, c.width, c.height, c.xhot, c.yhot, )?; Ok(ServerCursorTemplate { var: ServerCursorTemplateVariant::Static(Rc::new(cursor)), xcursor: cs, }) } else { let mut images = vec![]; for c in &cs { let img = CursorImage::from_bytes( ctx, &c.pixels, c.delay as _, c.width, c.height, c.xhot, c.yhot, )?; images.push(img); } Ok(ServerCursorTemplate { var: ServerCursorTemplateVariant::Animated(Rc::new(images)), xcursor: cs, }) } } Err(e) => { log::warn!("Could not load cursor {}: {}", name, ErrorFmt(e)); let empty: [Cell; 4] = unsafe { MaybeUninit::zeroed().assume_init() }; let cursor = CursorImage::from_bytes(ctx, &empty, 0, 1, 1, 0, 0)?; Ok(ServerCursorTemplate { var: ServerCursorTemplateVariant::Static(Rc::new(cursor)), xcursor: Default::default(), }) } } } pub fn instantiate(&self) -> Rc { match &self.var { ServerCursorTemplateVariant::Static(s) => Rc::new(StaticCursor { x: Cell::new(0), y: Cell::new(0), extents: Cell::new(s.extents), image: s.clone(), }), ServerCursorTemplateVariant::Animated(a) => { let mut start = c::timespec { tv_sec: 0, tv_nsec: 0, }; uapi::clock_gettime(c::CLOCK_MONOTONIC, &mut start).unwrap(); Rc::new(AnimatedCursor { start, next: NumCell::new(a[0].delay_ns), idx: Cell::new(0), images: a.clone(), x: Cell::new(0), y: Cell::new(0), extents: Cell::new(a[0].extents), }) } } } } struct CursorImage { extents: Rect, xhot: i32, yhot: i32, delay_ns: u64, tex: Rc, } impl CursorImage { fn from_bytes( ctx: &Rc, data: &[Cell], delay_ms: u64, width: i32, height: i32, xhot: i32, yhot: i32, ) -> Result { Ok(Self { extents: Rect::new_sized(-xhot, -yhot, width, height).unwrap(), xhot, yhot, delay_ns: delay_ms * 1_000_000, tex: ctx.shmem_texture(data, ARGB8888, width, height, width * 4)?, }) } } struct StaticCursor { x: Cell, y: Cell, extents: Cell, image: Rc, } impl Cursor for StaticCursor { fn set_position(&self, x: i32, y: i32) { let dx = x - self.x.replace(x); let dy = y - self.y.replace(y); self.extents.set(self.extents.get().move_(dx, dy)); } fn render(&self, renderer: &mut Renderer, x: i32, y: i32) { renderer.render_texture(&self.image.tex, x, y, ARGB8888); } fn get_hotspot(&self) -> (i32, i32) { (self.image.xhot, self.image.yhot) } fn extents(&self) -> Rect { self.extents.get() } } struct AnimatedCursor { start: c::timespec, next: NumCell, idx: Cell, images: Rc>, x: Cell, y: Cell, extents: Cell, } impl Cursor for AnimatedCursor { fn set_position(&self, x: i32, y: i32) { let dx = x - self.x.replace(x); let dy = y - self.y.replace(y); self.extents.set(self.extents.get().move_(dx, dy)); } fn render(&self, renderer: &mut Renderer, x: i32, y: i32) { let img = &self.images[self.idx.get()]; renderer.render_texture(&img.tex, x, y, ARGB8888); } fn get_hotspot(&self) -> (i32, i32) { let img = &self.images[self.idx.get()]; (img.xhot, img.yhot) } fn extents(&self) -> Rect { self.extents.get() } fn tick(&self) { let mut now = c::timespec { tv_sec: 0, tv_nsec: 0, }; uapi::clock_gettime(c::CLOCK_MONOTONIC, &mut now).unwrap(); let dist = (now.tv_sec.wrapping_sub(self.start.tv_sec)) as i64 * 1_000_000_000 + now.tv_nsec.wrapping_sub(self.start.tv_nsec) as i64; if (dist as u64) < self.next.get() { return; } let idx = (self.idx.get() + 1) % self.images.len(); self.idx.set(idx); let image = &self.images[idx]; self.extents.set( Rect::new_sized( self.x.get() - image.xhot, self.y.get() - image.yhot, image.extents.width(), image.extents.height(), ) .unwrap(), ); self.next.fetch_add(image.delay_ns); } } fn open_cursor( name: &str, theme: Option<&BStr>, size: u32, paths: &[BString], ) -> Result, CursorError> { let name = name.as_bytes().as_bstr(); let mut file = None; let mut themes_tested = AHashSet::new(); if let Some(theme) = theme { file = open_cursor_file(&mut themes_tested, paths, theme, name); } if file.is_none() { file = open_cursor_file(&mut themes_tested, paths, b"default".as_bstr(), name); } let file = match file { Some(f) => f, _ => return Err(CursorError::NotFound), }; let mut file = BufReader::new(file); let images = parser_cursor_file(&mut file, size)?; if images.is_empty() { return Err(CursorError::EmptyXcursorFile); } Ok(images) } fn open_cursor_file( themes_tested: &mut AHashSet, paths: &[BString], theme: &BStr, name: &BStr, ) -> Option { if !themes_tested.insert(theme.to_owned()) { return None; } if paths.is_empty() { return None; } let mut parents = None; for cursor_path in paths { let mut theme_dir = cursor_path.to_vec(); theme_dir.push(b'/'); theme_dir.extend_from_slice(theme.as_bytes()); let mut cursor_file = theme_dir.clone(); cursor_file.extend_from_slice(b"/cursors/"); cursor_file.extend_from_slice(name.as_bytes()); if let Ok(f) = File::open(cursor_file.to_os_str().unwrap()) { return Some(f); } if parents.is_none() { let mut index_file = theme_dir.clone(); index_file.extend_from_slice(b"/index.theme"); parents = find_parent_themes(&index_file); } } if let Some(parents) = parents { for parent in parents { if let Some(file) = open_cursor_file(themes_tested, paths, parent.as_bstr(), name) { return Some(file); } } } None } fn find_cursor_paths() -> Vec { let home = env::var_os(HOME).map(|h| Vec::from_os_string(h).unwrap()); let cursor_paths = env::var_os(XCURSOR_PATH); let cursor_paths = cursor_paths .as_ref() .map(|c| <[u8]>::from_os_str(c).unwrap()) .unwrap_or(XCURSOR_PATH_DEFAULT); let mut paths = vec![]; for path in <[u8]>::split(cursor_paths, |b| *b == b':') { if path.first() == Some(&b'~') { if let Some(home) = home.as_ref() { let mut full_path = home.clone(); full_path.extend_from_slice(&path[1..]); paths.push(full_path.into()); } else { log::warn!( "`HOME` is not set. Cannot expand {}. Ignoring.", path.as_bstr() ); } } else { paths.push(path.as_bstr().to_owned()); } } paths } fn find_parent_themes(path: &[u8]) -> Option> { // NOTE: The files we're reading here are really INI files with a hierarchy. This // algorithm treats it as a flat list and is inherited from libxcursor. let file = match File::open(path.to_os_str().unwrap()) { Ok(f) => f, _ => return None, }; let mut buf_reader = BufReader::new(file); let mut buf = vec![]; loop { buf.clear(); match buf_reader.read_until(b'\n', &mut buf) { Ok(n) if n > 0 => {} _ => return None, } let mut suffix = match buf.strip_prefix(b"Inherits") { Some(s) => s, _ => continue, }; while suffix.first() == Some(&b' ') { suffix = &suffix[1..]; } if suffix.first() != Some(&b'=') { continue; } suffix = &suffix[1..]; let parents = suffix .split(|b| matches!(*b, b' ' | b'\t' | b'\n' | b';' | b',')) .filter(|v| v.is_not_empty()) .map(|v| v.as_bstr().to_owned()) .collect(); return Some(parents); } } #[derive(Debug, Error)] pub enum CursorError { #[error("An IO error occurred: {0}")] Io(#[from] io::Error), #[error("The file is not an Xcursor file")] NotAnXcursorFile, #[error("The Xcursor file contains more than 0x10000 images")] OversizedXcursorFile, #[error("The Xcursor file is empty")] EmptyXcursorFile, #[error("The Xcursor file is corrupt")] CorruptXcursorFile, #[error("The requested cursor could not be found")] NotFound, #[error("Could not import the cursor as a texture")] ImportError(#[from] RenderError), } #[derive(Default, Clone)] pub struct XCursorImage { pub width: i32, pub height: i32, pub xhot: i32, pub yhot: i32, pub delay: u32, pub pixels: Vec>, } impl Debug for XCursorImage { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("XcbCursorImage") .field("width", &self.width) .field("height", &self.height) .field("xhot", &self.xhot) .field("yhot", &self.yhot) .field("delay", &self.delay) .finish_non_exhaustive() } } fn parser_cursor_file( r: &mut R, target: u32, ) -> Result, CursorError> { let [magic, header] = read_u32_n(r)?; if magic != XCURSOR_MAGIC || header < HEADER_SIZE { return Err(CursorError::NotAnXcursorFile); } let [_version, ntoc] = read_u32_n(r)?; r.seek(SeekFrom::Current((HEADER_SIZE - header) as i64))?; if ntoc > 0x10000 { return Err(CursorError::OversizedXcursorFile); } let mut images_positions = vec![]; let mut best_fit = i64::MAX; for _ in 0..ntoc { let [type_, size, position] = read_u32_n(r)?; if type_ != XCURSOR_IMAGE_TYPE { continue; } let fit = (size as i64 - target as i64).abs(); if fit < best_fit { best_fit = fit; images_positions.clear(); } if fit == best_fit { images_positions.push(position); } } let mut images = Vec::with_capacity(images_positions.len()); for position in images_positions { r.seek(SeekFrom::Start(position as u64))?; let [_chunk_header, _type_, _size, _version, width, height, xhot, yhot, delay] = read_u32_n(r)?; let [width, height, xhot, yhot] = u32_to_i32([width, height, xhot, yhot])?; let mut image = XCursorImage { width, height, xhot, yhot, delay, pixels: vec![], }; let num_bytes = width as usize * height as usize * 4; unsafe { image.pixels.reserve_exact(num_bytes as usize); image.pixels.set_len(num_bytes as usize); r.read_exact(slice::from_raw_parts_mut( image.pixels.as_mut_ptr() as _, num_bytes, ))?; } images.push(image); } Ok(images) } fn read_u32_n(r: &mut R) -> Result<[u32; N], io::Error> { let mut res = [0; N]; r.read_u32_into::(&mut res)?; Ok(res) } fn u32_to_i32(n: [u32; N]) -> Result<[i32; N], CursorError> { let mut res = [0; N]; for i in 0..N { res[i] = n[i] .try_into() .map_err(|_| CursorError::CorruptXcursorFile)?; } Ok(res) }