// SPDX-License-Identifier: GPL-3.0-only use std::{ cell::RefCell, io::Read, rc::Rc, sync::Mutex, }; use smithay::{ backend::{ renderer::{Frame, ImportAll, Renderer, Texture, gles2}, SwapBuffersError, }, desktop::space::{RenderElement, SpaceOutputTuple, SurfaceTree, DynamicRenderElements}, reexports::wayland_server::protocol::wl_surface, utils::{Logical, Buffer, Point, Rectangle, Size, Transform}, wayland::{ compositor::{get_role, with_states}, seat::{Seat, CursorImageAttributes, CursorImageStatus}, }, }; use xcursor::{ parser::{parse_xcursor, Image}, CursorTheme, }; use crate::state::get_dnd_icon; static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../../resources/cursor.rgba"); #[derive(Debug, Clone)] pub struct Cursor { icons: Vec, size: u32, } impl Cursor { pub fn load() -> Cursor { let name = std::env::var("XCURSOR_THEME") .ok() .unwrap_or_else(|| "default".into()); let size = std::env::var("XCURSOR_SIZE") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(24); let theme = CursorTheme::load(&name); let icons = load_icon(&theme) .map_err(|err| slog_scope::warn!("Unable to load xcursor: {}, using fallback cursor", err)) .unwrap_or_else(|_| { vec![Image { size: 32, width: 64, height: 64, xhot: 1, yhot: 1, delay: 1, pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA), pixels_argb: vec![], //unused }] }); Cursor { icons, size } } pub fn get_image(&self, scale: u32, millis: u32) -> Image { let size = self.size * scale; frame(millis, size, &self.icons) } } impl Default for Cursor { fn default() -> Cursor { Cursor::load() } } fn nearest_images(size: u32, images: &[Image]) -> impl Iterator { // Follow the nominal size of the cursor to choose the nearest 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) } fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image { let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay); millis %= total; for img in nearest_images(size, images) { if millis < img.delay { return img.clone(); } millis -= img.delay; } unreachable!() } #[derive(thiserror::Error, Debug)] enum Error { #[error("Theme has no default cursor")] NoDefaultCursor, #[error("Error opening xcursor file: {0}")] File(#[from] std::io::Error), #[error("Failed to parse XCursor file")] Parse, } fn load_icon(theme: &CursorTheme) -> Result, Error> { let icon_path = theme.load_icon("default").ok_or(Error::NoDefaultCursor)?; let mut cursor_file = std::fs::File::open(&icon_path)?; let mut cursor_data = Vec::new(); cursor_file.read_to_end(&mut cursor_data)?; parse_xcursor(&cursor_data).ok_or(Error::Parse) } pub fn draw_surface_cursor( surface: wl_surface::WlSurface, location: impl Into>, ) -> impl RenderElement where R: Renderer + ImportAll + 'static, F: Frame + 'static, E: std::error::Error + Into + 'static, T: Texture + 'static, { let mut position = location.into(); let ret = with_states(&surface, |states| { Some( states .data_map .get::>() .unwrap() .lock() .unwrap() .hotspot, ) }) .unwrap_or(None); position -= match ret { Some(h) => h, None => { slog_scope::warn!("Trying to display as a cursor a surface that does not have the CursorImage role."); (0, 0).into() } }; SurfaceTree { surface, position } } pub fn draw_dnd_icon( surface: wl_surface::WlSurface, location: impl Into>, ) -> impl RenderElement where R: Renderer + ImportAll + 'static, F: Frame + 'static, E: std::error::Error + Into + 'static, T: Texture + 'static, { if get_role(&surface) != Some("dnd_icon") { slog_scope::warn!("Trying to display as a dnd icon a surface that does not have the DndIcon role."); } SurfaceTree { surface, position: location.into(), } } pub struct PointerElement { texture: T, position: Point, size: Size, new_frame: bool, } impl PointerElement { pub fn new(texture: T, relative_pointer_pos: Point, new_frame: bool) -> PointerElement { let size = texture.size().to_logical(1, Transform::Normal); PointerElement { texture, position: relative_pointer_pos, size, new_frame, } } } impl RenderElement for PointerElement where R: Renderer + ImportAll + 'static, F: Frame + 'static, E: std::error::Error + Into + 'static, T: Texture + 'static, { fn id(&self) -> usize { 0 } fn geometry(&self) -> Rectangle { Rectangle::from_loc_and_size(self.position, self.size) } fn accumulated_damage(&self, _: Option>) -> Vec> { if self.new_frame { vec![Rectangle::from_loc_and_size((0, 0), self.size)] } else { vec![] } } fn draw( &self, _renderer: &mut R, frame: &mut F, scale: f64, position: Point, damage: &[Rectangle], _log: &slog::Logger, ) -> Result<(), R::Error> { frame.render_texture_at( &self.texture, position.to_f64().to_physical(scale as f64).to_i32_round(), 1, scale as f64, Transform::Normal, &*damage .iter() .map(|rect| rect.to_buffer(1, Transform::Normal, &self.size)) .collect::>(), 1.0, )?; Ok(()) } } #[derive(Debug, Default)] struct CursorState { cursor: Cursor, current_image: RefCell>, } pub type Textures = Vec<(Image, gles2::Gles2Texture)>; pub fn draw_cursor( renderer: &mut gles2::Gles2Renderer, seat: &Seat, start_time: &std::time::Instant, draw_default: bool, ) -> Option> { let pointer = match seat.get_pointer() { Some(ptr) => ptr, None => return None, }; let location = pointer.current_location(); // draw the dnd icon if applicable { if let Some(wl_surface) = get_dnd_icon(seat) { if wl_surface.as_ref().is_alive() { return Some(Box::new(draw_dnd_icon( wl_surface, location.to_i32_round(), ))); } } } // draw the cursor as relevant { // reset the cursor if the surface is no longer alive let cursor_status = seat .user_data() .get::>() .map(|cell| { let mut cursor_status = cell.borrow_mut(); if let CursorImageStatus::Image(ref surface) = *cursor_status { if !surface.as_ref().is_alive() { *cursor_status = CursorImageStatus::Default; } } cursor_status.clone() }) .unwrap_or(CursorImageStatus::Default); if let CursorImageStatus::Image(wl_surface) = cursor_status { Some(Box::new(draw_surface_cursor( wl_surface.clone(), location.to_i32_round(), ))) } else if draw_default { let seat_userdata = seat.user_data(); seat_userdata.insert_if_missing(CursorState::default); let state = seat_userdata.get::().unwrap(); let frame = state.cursor.get_image(1, start_time.elapsed().as_millis() as u32); let new_frame = state.current_image.borrow().as_ref() != Some(&frame); let egl_userdata = renderer.egl_context().user_data(); egl_userdata.insert_if_missing(|| Rc::new(RefCell::new(Textures::new()))); let pointer_images = egl_userdata.get::>>().unwrap().clone(); let pointer_images_ref = &mut *pointer_images.borrow_mut(); let pointer_image = pointer_images_ref .iter() .find_map(|(image, texture)| if image == &frame { Some(texture) } else { None }) .cloned() .unwrap_or_else(|| { let texture = import_bitmap(renderer, &frame.pixels_rgba, gles2::ffi::RGBA, (frame.width as i32, frame.height as i32)) .expect("Failed to import cursor bitmap"); pointer_images_ref.push((frame.clone(), texture.clone())); texture }); let hotspot = Point::::from((frame.xhot as i32, frame.yhot as i32)); *state.current_image.borrow_mut() = Some(frame); Some(Box::new(PointerElement::new( pointer_image.clone(), location.to_i32_round() - hotspot, new_frame, ))) } else { None } } } pub fn import_bitmap( renderer: &mut gles2::Gles2Renderer, image: impl std::convert::AsRef<[u8]>, format: gles2::ffi::types::GLenum, size: impl Into>, ) -> Result { use smithay::backend::renderer::gles2::ffi; let size = size.into(); renderer.with_context(|renderer, gl| unsafe { let mut tex = 0; gl.GenTextures(1, &mut tex); gl.BindTexture(ffi::TEXTURE_2D, tex); gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_S, ffi::CLAMP_TO_EDGE as i32); gl.TexParameteri(ffi::TEXTURE_2D, ffi::TEXTURE_WRAP_T, ffi::CLAMP_TO_EDGE as i32); gl.TexImage2D( ffi::TEXTURE_2D, 0, format as i32, size.w, size.h, 0, format, ffi::UNSIGNED_BYTE as u32, image.as_ref().as_ptr() as *const _, ); gl.BindTexture(ffi::TEXTURE_2D, 0); gles2::Gles2Texture::from_raw( renderer, tex, size, ) }) }