//! Rasteryzacja tekstu do tekstury GL (Cairo → `MemoryRenderBuffer`). //! //! PO CO: kompozytor rysuje pasek tytułu dla WSZYSTKICH okien (wymuszony SSD). //! Żeby dekoracja była kompletna (tytuł okna oraz etykiety menu aplikacji: //! Plik/Edycja/Widok/Pomoc), kompozytor musi umieć narysować TEKST — inaczej //! pasek jest „ślepy”. //! //! DLACZEGO Cairo: to ta sama biblioteka, której używa nasz toolkit, i jest już //! spakowana w PaganOS. Nie dokładamy więc nowej zależności systemowej. //! //! DLACZEGO cache: rasteryzacja napisu jest kosztowna, a teksty (tytuły, etykiety //! menu) zmieniają się rzadko. Trzymamy gotowe tekstury kluczowane treścią, //! rozmiarem czcionki i kolorem — klatka tylko je składa. //! //! FORMAT: Cairo `ARgb32` jest premultiplied, a renderer Smithaya miesza kolory //! funkcją `ONE, ONE_MINUS_SRC_ALPHA` (czyli też premultiplied) — dlatego bajty //! Cairo można wysłać na GPU bez żadnej konwersji, jako `Fourcc::Argb8888`. use std::{ collections::HashMap, sync::{Mutex, OnceLock}, }; use cairo::{Context, FontSlant, FontWeight, Format, ImageSurface}; use smithay::{ backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer}, utils::{Logical, Size, Transform}, }; /// Gotowa tekstura napisu wraz z jej rozmiarem w pikselach logicznych. #[derive(Clone)] pub struct TextTexture { /// Bufor pamięciowy gotowy do importu przez renderer. pub buffer: MemoryRenderBuffer, /// Rozmiar tekstury (szerokość × wysokość). pub size: Size, } /// Klucz cache — dokładnie to, co wpływa na wygląd tekstury. #[derive(PartialEq, Eq, Hash, Clone)] struct Key { text: String, font_px: i32, color: [u8; 3], } fn cache() -> &'static Mutex>> { static CACHE: OnceLock>>> = OnceLock::new(); CACHE.get_or_init(|| Mutex::new(HashMap::new())) } /// Cache samych POMIARÓW (szerokość napisu) — używany przy układaniu menu, /// gdzie potrzebujemy rozmiaru, a nie tekstury. fn measures() -> &'static Mutex>>> { static MEASURES: OnceLock>>>> = OnceLock::new(); MEASURES.get_or_init(|| Mutex::new(HashMap::new())) } /// Zwraca (z cache) rozmiar napisu w pikselach, bez tworzenia tekstury. /// /// PO CO osobno: układ menu (pozycje etykiet) liczymy także przy obsłudze myszy, /// gdzie renderera nie ma pod ręką — wystarczy nam wtedy sam pomiar. pub fn measure(text: &str, font_px: f64) -> Option> { if text.trim().is_empty() { return None; } let key = (text.to_string(), font_px.round().max(1.0) as i32); if let Some(cached) = measures() .lock() .expect("cache pomiarów zatruty") .get(&key) .cloned() { return cached; } let result = measure_uncached(text, font_px); measures() .lock() .expect("cache pomiarów zatruty") .insert(key, result); result } fn measure_uncached(text: &str, font_px: f64) -> Option> { let dummy = ImageSurface::create(Format::ARgb32, 1, 1).ok()?; let ctx = Context::new(&dummy).ok()?; ctx.select_font_face("sans-serif", FontSlant::Normal, FontWeight::Normal); ctx.set_font_size(font_px); let ink = ctx.text_extents(text).ok()?; let font = ctx.font_extents().ok()?; Some(Size::from(( ink.width().ceil() as i32, (font.ascent() + font.descent()).ceil() as i32, ))) } /// Zwraca (z cache) teksturę napisu. `None`, gdy rasteryzacja się nie powiodła — /// wtedy wołający po prostu nie rysuje tekstu (dekoracja działa dalej). pub fn texture(text: &str, font_px: f64, color: (f64, f64, f64)) -> Option { if text.trim().is_empty() { return None; } let key = Key { text: text.to_string(), font_px: font_px.round().max(1.0) as i32, color: [ (color.0.clamp(0.0, 1.0) * 255.0).round() as u8, (color.1.clamp(0.0, 1.0) * 255.0).round() as u8, (color.2.clamp(0.0, 1.0) * 255.0).round() as u8, ], }; if let Some(cached) = cache() .lock() .expect("cache tekstu zatruty") .get(&key) .cloned() { return cached; } let raster = rasterize(text, key.font_px as f64, color); if raster.is_none() { tracing::warn!(text, "nie udało się zrasteryzować tekstu"); } cache() .lock() .expect("cache tekstu zatruty") .insert(key, raster.clone()); raster } /// Właściwa rasteryzacja: mierzy napis, tworzy powierzchnię i rysuje tekst. fn rasterize(text: &str, font_px: f64, color: (f64, f64, f64)) -> Option { /// Zapas wokół napisu, żeby antyaliasing nie został obcięty na krawędzi. const PADDING: i32 = 2; // 1) Pomiar na powierzchni pomocniczej (Cairo nie mierzy tekstu bez kontekstu). let dummy = ImageSurface::create(Format::ARgb32, 1, 1).ok()?; let measure = Context::new(&dummy).ok()?; measure.select_font_face("sans-serif", FontSlant::Normal, FontWeight::Normal); measure.set_font_size(font_px); let ink = measure.text_extents(text).ok()?; let font = measure.font_extents().ok()?; let width = ink.width().ceil() as i32 + PADDING * 2 + 1; let height = (font.ascent() + font.descent()).ceil() as i32 + PADDING * 2; if width <= 0 || height <= 0 { return None; } // 2) Właściwa powierzchnia + rysunek. let surface = ImageSurface::create(Format::ARgb32, width, height).ok()?; { let ctx = Context::new(&surface).ok()?; ctx.set_source_rgb(color.0, color.1, color.2); ctx.select_font_face("sans-serif", FontSlant::Normal, FontWeight::Normal); ctx.set_font_size(font_px); ctx.move_to(PADDING as f64, PADDING as f64 + font.ascent()); ctx.show_text(text).ok()?; } surface.flush(); // 3) Kopiujemy bajty, zwijając wiersze do `width * 4`, gdyby Cairo dodał // wyrównanie (dla ARGB32 zwykle go nie ma, ale nie zakładamy tego). let stride = surface.stride(); let row_bytes = width * 4; let mut data = Vec::with_capacity((row_bytes * height) as usize); surface .with_data(|raw| { if stride == row_bytes { data.extend_from_slice(raw); } else { for row in raw.chunks_exact(stride as usize).take(height as usize) { data.extend_from_slice(&row[..row_bytes as usize]); } } }) .ok()?; let (buffer, size) = upload(&data, width, height)?; Some(TextTexture { buffer, size }) } /// Wysyła bajty powierzchni Cairo (ARGB32, premultiplied) jako teksturę GL. /// /// PO CO wspólny helper: korzystają z niego i tekst ([`texture`]), i ikony /// (`crate::icons`) — konwersja formatu i wyrównanie wierszy muszą być identyczne. pub(crate) fn upload( data: &[u8], width: i32, height: i32, ) -> Option<(MemoryRenderBuffer, Size)> { if data.len() < (width * height * 4) as usize { return None; } let buffer = MemoryRenderBuffer::from_slice( data, Fourcc::Argb8888, (width, height), 1, Transform::Normal, None, ); Some((buffer, Size::from((width, height)))) }