Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

protocol: add cursor_shape #32

Merged
merged 1 commit into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
346 changes: 270 additions & 76 deletions src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason it's being stored in Rc? It's not like using references won't really work when providing the cached item, given that it worked just fine before.

The only benefit of Rc which you're not using is to spread the default cursor into other variants.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed &self in get() signature, so I need this inside a RefCell. So the get() would need to return a Ref or RefMut. But I couldn't map RefMut<HashMap> into a RefMut<Option<&XCursor>>, only a RefMut<Option<XCursor>>, which is not what I wanted. So I changed it to an Rc. Basically, battle lost with the borrow checker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you see, RefCell is evil, could just have &mut and it'll be more or less fine...


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()
}
}
3 changes: 2 additions & 1 deletion src/handlers/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ impl CompositorHandler for State {
self.layer_shell_handle_commit(surface);

// This might be a cursor surface.
if matches!(&self.niri.cursor_image, CursorImageStatus::Surface(s) if s == surface) {
if matches!(&self.niri.cursor_manager.cursor_image(), CursorImageStatus::Surface(s) if s == surface)
{
// FIXME: granular redraws for cursors.
self.niri.queue_redraw_all();
}
Expand Down
Loading