//! 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>>>, } 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::().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> { 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> { 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 { 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 { 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 { 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 = OnceLock::new(); THEME.get_or_init(Theme::load) }