🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
//! 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<i32, Logical>,
}
/// 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<HashMap<Key, Option<TextTexture>>> {
static CACHE: OnceLock<Mutex<HashMap<Key, Option<TextTexture>>>> = 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<HashMap<(String, i32), Option<Size<i32, Logical>>>> {
static MEASURES: OnceLock<Mutex<HashMap<(String, i32), Option<Size<i32, Logical>>>>> =
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<Size<i32, Logical>> {
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<Size<i32, Logical>> {
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<TextTexture> {
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<TextTexture> {
/// 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<i32, Logical>)> {
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))))
}