🔒 Repository is read-only – file editing is disabled.

PaganDE/pagan-compositor/src/cursor.rs main

159 linii Raw ← Powrót
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)
}