🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
//! Kursor z motywu XCursor (zamiast rysowanego prostokąta).
//!
//! PO CO: kompozytor desktopowy powinien pokazywać właściwy kursor (strzałka,
//! „łapka”, I-beam, kursor zmiany rozmiaru) z motywu systemowego. Wcześniej
//! rysowaliśmy zastępczy prostokąt — działał, ale wyglądał obco.
//!
//! Źródło motywu to `XCURSOR_THEME` / `XCURSOR_SIZE` (standardowe zmienne
//! środowiska), a same pliki czytamy przez crate `xcursor` (biblioteka
//! `libXcursor`). Gdy motywu brak, `buffer()` zwraca `None` — kompozytor
//! wraca wtedy do prostokąta, więc brak motywu nie psuje sesji.
use std::{
collections::HashMap,
io::Read,
sync::{Mutex, OnceLock},
time::Duration,
};
use smithay::{
backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer},
input::pointer::CursorIcon,
utils::Transform,
};
use xcursor::{
parser::{parse_xcursor, Image},
CursorTheme,
};
/// Motyw kursora + cache wczytanych ikon.
///
/// PO CO cache: parsowanie pliku ikony przy KAŻDEJ klatce byłoby marnotrawstwem
/// (setki razy na sekundę). Wczytujemy raz na nazwę ikony.
pub struct Theme {
theme: CursorTheme,
/// Nominalny rozmiar kursora (z `XCURSOR_SIZE`).
size: u32,
cache: Mutex<HashMap<String, Option<Vec<Image>>>>,
}
impl Theme {
fn load() -> Self {
let name = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".to_string());
let size = std::env::var("XCURSOR_SIZE")
.ok()
.and_then(|value| value.trim().parse::<u32>().ok())
.filter(|value| *value > 0)
.unwrap_or(24);
tracing::info!(theme = %name, size, "ładowanie motywu kursora");
Self {
theme: CursorTheme::load(&name),
size,
cache: Mutex::new(HashMap::new()),
}
}
/// Wczytuje (z cache) ikony o danej nazwie, np. `default`, `text`, `pointer`.
fn icons(&self, name: &str) -> Option<Vec<Image>> {
if let Some(cached) = self.cache.lock().unwrap().get(name) {
return cached.clone();
}
let loaded = self.load_icon(name);
if loaded.is_none() {
tracing::debug!(icon = name, "brak ikony w motywie kursora");
}
self.cache
.lock()
.unwrap()
.insert(name.to_string(), loaded.clone());
loaded
}
fn load_icon(&self, name: &str) -> Option<Vec<Image>> {
let path = self.theme.load_icon(name)?;
let mut data = Vec::new();
std::fs::File::open(path)
.ok()?
.read_to_end(&mut data)
.ok()?;
parse_xcursor(&data)
}
/// Wybiera konkretną klatkę obrazu dla ikony (z awaryjnym `default`).
pub fn image(&self, icon: CursorIcon, scale: u32, time: Duration) -> Option<Image> {
let name = icon.name();
// `or_else` na `default`: część ikon (np. egzotyczne kształty resize)
// bywa nieobecna w motywie — lepiej pokazać strzałkę niż nic.
let icons = self.icons(name).or_else(|| {
if name == "default" {
None
} else {
self.icons("default")
}
})?;
frame(time.as_millis() as u32, self.size * scale.max(1), &icons)
}
/// Buduje bufor do narysowania kursora + punkt „hotspot” (czubek strzałki).
///
/// Zwraca `None`, gdy nie ma motywu — wołający użyje wtedy prostokąta.
pub fn buffer(
&self,
icon: CursorIcon,
scale: u32,
time: Duration,
) -> Option<(MemoryRenderBuffer, (i32, i32))> {
let image = self.image(icon, scale, time)?;
let buffer = MemoryRenderBuffer::from_slice(
&image.pixels_rgba,
Fourcc::Argb8888,
(image.width as i32, image.height as i32),
1,
Transform::Normal,
None,
);
Some((buffer, (image.xhot as i32, image.yhot as i32)))
}
}
/// Ikony najbliższe zadanemu rozmiarowi nominalnemu (XCursor trzyma kilka wariantów).
fn nearest_images(size: u32, images: &[Image]) -> impl Iterator<Item = &Image> {
let nearest = images
.iter()
.min_by_key(|image| (size as i32 - image.size as i32).abs());
let (w, h) = nearest
.map(|image| (image.width, image.height))
.unwrap_or((0, 0));
images
.iter()
.filter(move |image| image.width == w && image.height == h)
}
/// Wybiera klatkę animacji kursora dla danego czasu.
fn frame(millis: u32, size: u32, images: &[Image]) -> Option<Image> {
let total: u32 = nearest_images(size, images).map(|image| image.delay).sum();
if total == 0 {
return nearest_images(size, images).next().cloned();
}
let mut remaining = millis % total;
for image in nearest_images(size, images) {
if remaining < image.delay {
return Some(image.clone());
}
remaining -= image.delay;
}
nearest_images(size, images).next().cloned()
}
/// Globalny motyw kursora — ładowany leniwie, dokładnie raz.
///
/// PO CO globalnie: motyw jest jeden dla całego kompozytora, a trzymanie go
/// w stanie backendu zmuszałoby do plątania typów zależnych od backendu.
pub fn theme() -> &'static Theme {
static THEME: OnceLock<Theme> = OnceLock::new();
THEME.get_or_init(Theme::load)
}