130 lines
3.7 KiB
Rust
130 lines
3.7 KiB
Rust
use std::fs;
|
|
use std::sync::OnceLock;
|
|
|
|
use smithay::backend::allocator::Fourcc;
|
|
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
|
use smithay::input::pointer::CursorIcon;
|
|
use smithay::utils::{Buffer, Point, Size, Transform};
|
|
use xcursor::CursorTheme;
|
|
use xcursor::parser::Image;
|
|
|
|
const DEFAULT_CURSOR_SIZE: u32 = 24;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct NamedCursor {
|
|
pub buffer: MemoryRenderBuffer,
|
|
pub hotspot: Point<i32, Buffer>,
|
|
}
|
|
|
|
pub(crate) fn cursor_theme() -> CursorTheme {
|
|
let name = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into());
|
|
|
|
CursorTheme::load(&name)
|
|
}
|
|
|
|
pub(crate) fn load_named_cursor(theme: &CursorTheme, icon: CursorIcon) -> NamedCursor {
|
|
let requested_size = cursor_size();
|
|
let image = cursor_names(icon)
|
|
.iter()
|
|
.find_map(|name| load_image(theme, name, requested_size));
|
|
|
|
match image {
|
|
Some(image) => cursor_from_image(image),
|
|
None => fallback_cursor(),
|
|
}
|
|
}
|
|
|
|
fn cursor_size() -> u32 {
|
|
static SIZE: OnceLock<u32> = OnceLock::new();
|
|
|
|
*SIZE.get_or_init(|| {
|
|
std::env::var("XCURSOR_SIZE")
|
|
.ok()
|
|
.and_then(|value| value.parse().ok())
|
|
.filter(|size| *size > 0)
|
|
.unwrap_or(DEFAULT_CURSOR_SIZE)
|
|
})
|
|
}
|
|
|
|
fn cursor_names(icon: CursorIcon) -> [&'static str; 3] {
|
|
match icon {
|
|
CursorIcon::Default => ["default", "left_ptr", "arrow"],
|
|
CursorIcon::Pointer => ["pointer", "hand2", "left_ptr"],
|
|
CursorIcon::Text => ["text", "xterm", "left_ptr"],
|
|
_ => [icon.name(), "default", "left_ptr"],
|
|
}
|
|
}
|
|
|
|
fn load_image(theme: &CursorTheme, name: &str, requested_size: u32) -> Option<Image> {
|
|
let path = theme.load_icon(name)?;
|
|
let bytes = fs::read(path).ok()?;
|
|
|
|
xcursor::parser::parse_xcursor(&bytes)?.into_iter().min_by_key(|image| {
|
|
(
|
|
image.size.abs_diff(requested_size),
|
|
image.width.abs_diff(requested_size),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn cursor_from_image(image: Image) -> NamedCursor {
|
|
let size = Size::<i32, Buffer>::from((image.width as i32, image.height as i32));
|
|
let buffer = MemoryRenderBuffer::from_slice(&image.pixels_rgba, Fourcc::Abgr8888, size, 1, Transform::Normal, None);
|
|
|
|
NamedCursor {
|
|
buffer,
|
|
hotspot: Point::from((image.xhot as i32, image.yhot as i32)),
|
|
}
|
|
}
|
|
|
|
fn fallback_cursor() -> NamedCursor {
|
|
const WIDTH: usize = 12;
|
|
const HEIGHT: usize = 18;
|
|
let mut pixels = vec![0_u8; WIDTH * HEIGHT * 4];
|
|
|
|
for y in 0..HEIGHT {
|
|
let body_width = (y / 2 + 1).min(8);
|
|
|
|
for x in 0..body_width {
|
|
let edge = x == 0 || x + 1 == body_width || y == 0;
|
|
let offset = (y * WIDTH + x) * 4;
|
|
let color = if edge { 0 } else { 255 };
|
|
pixels[offset..offset + 4].copy_from_slice(&[color, color, color, 255]);
|
|
}
|
|
}
|
|
|
|
NamedCursor {
|
|
buffer: MemoryRenderBuffer::from_slice(
|
|
&pixels,
|
|
Fourcc::Abgr8888,
|
|
Size::<i32, Buffer>::from((WIDTH as i32, HEIGHT as i32)),
|
|
1,
|
|
Transform::Normal,
|
|
None,
|
|
),
|
|
hotspot: Point::default(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use smithay::input::pointer::CursorIcon;
|
|
|
|
use super::cursor_names;
|
|
|
|
#[test]
|
|
fn supplies_legacy_fallback_names_for_common_cursors() {
|
|
assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]);
|
|
assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]);
|
|
assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]);
|
|
}
|
|
|
|
#[test]
|
|
fn falls_back_to_default_for_other_named_cursors() {
|
|
assert_eq!(
|
|
cursor_names(CursorIcon::Crosshair),
|
|
["crosshair", "default", "left_ptr"]
|
|
);
|
|
}
|
|
}
|