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

luna/crates/libluna-render/src/scene.rs main

328 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
//! Graf sceny — opis tego, co ma zostać narysowane, w pikselach logicznych.
//!
//! Scena jest niezależna od backendu: budują ją widgety, a [`crate::SoftwareRenderer`]
//! (albo przyszły backend GPU) ją rasteryzuje.

use libluna_core::style::weight;
use libluna_core::{Color, Point, Rect};
use std::sync::Arc;

/// Gotowy obraz (premultiplied RGBA8) — np. skomponowany bufor okna z `wl_shm`.
#[derive(Debug, Clone, PartialEq)]
pub struct ImageData {
    pub width: u32,
    pub height: u32,
    /// Piksele premultiplied RGBA8, dokładnie `width * height * 4` bajtów.
    pub rgba: Vec<u8>,
    /// Czy obraz jest **rzadki** (dużo w pełni przezroczystych pikseli).
    /// Renderer używa wtedy blitowania z pomijaniem `alpha == 0` zamiast
    /// blendowania całego prostokąta — panele i popupy mają >50% pustych pikseli.
    pub sparse: bool,
}

impl ImageData {
    pub fn new(width: u32, height: u32, rgba: Vec<u8>) -> Self {
        Self {
            width,
            height,
            rgba,
            sparse: false,
        }
    }

    /// Oznacza obraz jako rzadki (patrz [`ImageData::sparse`]).
    pub fn with_sparse(mut self, sparse: bool) -> Self {
        self.sparse = sparse;
        self
    }

    /// Pusty (przezroczysty) obraz o zadanym rozmiarze.
    pub fn empty(width: u32, height: u32) -> Self {
        Self {
            width,
            height,
            rgba: vec![0; width as usize * height as usize * 4],
            sparse: true,
        }
    }
}

/// Pojedynczy fragment tekstu do narysowania.
#[derive(Debug, Clone, PartialEq)]
pub struct TextRun {
    /// Lewy-górny róg obszaru tekstu; `origin.y` to linia bazowa pierwszego wiersza.
    pub origin: Point,
    pub text: String,
    /// Rozmiar czcionki w pikselach logicznych.
    pub size: f64,
    /// Grubość (patrz [`libluna_core::style::weight`]).
    pub weight: u16,
    pub color: Color,
}

impl TextRun {
    pub fn new(origin: Point, text: impl Into<String>, size: f64) -> Self {
        Self {
            origin,
            text: text.into(),
            size,
            weight: weight::NORMAL,
            color: Color::BLACK,
        }
    }

    pub fn with_color(mut self, color: Color) -> Self {
        self.color = color;
        self
    }

    pub fn with_weight(mut self, weight: u16) -> Self {
        self.weight = weight;
        self
    }
}

/// Polecenie rysowania.
#[derive(Debug, Clone, PartialEq)]
pub enum DrawCommand {
    /// Wypełniony prostokąt.
    FillRect { rect: Rect, color: Color },
    /// Wypełniony prostokąt z zaokrąglonymi narożnikami.
    FillRoundedRect {
        rect: Rect,
        radius: f64,
        color: Color,
    },
    /// Obramowanie zaokrąglonego prostokąta (linia o zadanej grubości).
    StrokeRoundedRect {
        rect: Rect,
        radius: f64,
        width: f64,
        color: Color,
    },
    /// Odcinek o zadanej grubości (z zaokrąglonymi końcami).
    StrokeLine {
        from: Point,
        to: Point,
        width: f64,
        color: Color,
    },
    /// Wypełniony okrąg.
    FillCircle {
        center: Point,
        radius: f64,
        color: Color,
    },
    /// Obwód okręgu (linia o zadanej grubości).
    StrokeCircle {
        center: Point,
        radius: f64,
        width: f64,
        color: Color,
    },
    /// Łamana (otwarta; powtórz pierwszy punkt, by zamknąć kontur).
    StrokePolyline {
        points: Vec<Point>,
        width: f64,
        color: Color,
    },
    /// Wypełniony wielokąt (domykany automatycznie).
    FillPolygon { points: Vec<Point>, color: Color },
    /// Ogranicza kolejne polecenia do prostokąta (zastosowanie kumulacyjne).
    PushClip { rect: Rect },
    /// Ogranicza kolejne polecenia do **zaokrąglonego** prostokąta (kumulacyjnie).
    PushClipRounded { rect: Rect, radius: f64 },
    /// Zdejmuje ostatnie ograniczenie.
    PopClip,
    /// Tekst.
    Text(TextRun),
    /// Obraz (np. bufor okna) wpisany w prostokąt docelowy.
    Image { rect: Rect, data: Arc<ImageData> },
    /// Tło pulpitu: pełnoekranowy, **nieprzezroczysty** obraz. Renderer kopiuje piksele
    /// wprost — mieszanie 2 mln pikseli co klatkę kosztowało ~10 ms na 1080p.
    Background { data: Arc<ImageData> },
}

/// Uporządkowana lista poleceń rysowania.
#[derive(Debug, Clone, Default)]
pub struct Scene {
    commands: Vec<DrawCommand>,
}

impl Scene {
    pub fn new() -> Self {
        Self {
            commands: Vec::new(),
        }
    }

    /// Czyści scenę do ponownego użycia (bez zwalniania pamięci).
    pub fn clear(&mut self) {
        self.commands.clear();
    }

    pub fn len(&self) -> usize {
        self.commands.len()
    }

    pub fn is_empty(&self) -> bool {
        self.commands.is_empty()
    }

    pub fn commands(&self) -> &[DrawCommand] {
        &self.commands
    }

    pub fn fill_rect(&mut self, rect: Rect, color: Color) -> &mut Self {
        self.commands.push(DrawCommand::FillRect { rect, color });
        self
    }

    pub fn fill_rounded_rect(&mut self, rect: Rect, radius: f64, color: Color) -> &mut Self {
        self.commands.push(DrawCommand::FillRoundedRect {
            rect,
            radius,
            color,
        });
        self
    }

    pub fn stroke_rounded_rect(
        &mut self,
        rect: Rect,
        radius: f64,
        width: f64,
        color: Color,
    ) -> &mut Self {
        self.commands.push(DrawCommand::StrokeRoundedRect {
            rect,
            radius,
            width,
            color,
        });
        self
    }

    pub fn stroke_line(&mut self, from: Point, to: Point, width: f64, color: Color) -> &mut Self {
        self.commands.push(DrawCommand::StrokeLine {
            from,
            to,
            width,
            color,
        });
        self
    }

    /// Wypełniony okrąg.
    pub fn fill_circle(&mut self, center: Point, radius: f64, color: Color) -> &mut Self {
        self.commands.push(DrawCommand::FillCircle {
            center,
            radius,
            color,
        });
        self
    }

    /// Obwód okręgu.
    pub fn stroke_circle(
        &mut self,
        center: Point,
        radius: f64,
        width: f64,
        color: Color,
    ) -> &mut Self {
        self.commands.push(DrawCommand::StrokeCircle {
            center,
            radius,
            width,
            color,
        });
        self
    }

    /// Łamana (szkielet ikon); z zaokrąglonymi końcami i zgięciami.
    pub fn stroke_polyline(&mut self, points: &[Point], width: f64, color: Color) -> &mut Self {
        self.commands.push(DrawCommand::StrokePolyline {
            points: points.to_vec(),
            width,
            color,
        });
        self
    }

    /// Wypełniony wielokąt (domykany automatycznie).
    pub fn fill_polygon(&mut self, points: &[Point], color: Color) -> &mut Self {
        self.commands.push(DrawCommand::FillPolygon {
            points: points.to_vec(),
            color,
        });
        self
    }

    pub fn push_clip(&mut self, rect: Rect) -> &mut Self {
        self.commands.push(DrawCommand::PushClip { rect });
        self
    }

    /// Ogranicza kolejne polecenia do zaokrąglonego prostokąta (np. narożniki okna).
    pub fn push_clip_rounded(&mut self, rect: Rect, radius: f64) -> &mut Self {
        self.commands
            .push(DrawCommand::PushClipRounded { rect, radius });
        self
    }

    pub fn pop_clip(&mut self) -> &mut Self {
        self.commands.push(DrawCommand::PopClip);
        self
    }

    pub fn text(&mut self, run: TextRun) -> &mut Self {
        self.commands.push(DrawCommand::Text(run));
        self
    }

    /// Rysuje obraz wpisany w `rect` (skalowany, jeśli rozmiary się różnią).
    pub fn image(&mut self, rect: Rect, data: Arc<ImageData>) -> &mut Self {
        self.commands.push(DrawCommand::Image { rect, data });
        self
    }

    /// Ustawia tło pulpitu (pełnoekranowy, nieprzezroczysty obraz).
    pub fn background(&mut self, data: Arc<ImageData>) -> &mut Self {
        self.commands.push(DrawCommand::Background { data });
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builds_commands_in_order() {
        let mut s = Scene::new();
        s.fill_rect(Rect::from_xywh(0.0, 0.0, 10.0, 10.0), Color::BLACK)
            .push_clip(Rect::from_xywh(0.0, 0.0, 5.0, 5.0))
            .pop_clip();
        assert_eq!(s.len(), 3);
        assert!(matches!(s.commands()[0], DrawCommand::FillRect { .. }));
        assert!(matches!(s.commands()[1], DrawCommand::PushClip { .. }));
        assert!(matches!(s.commands()[2], DrawCommand::PopClip));
    }

    /// Scena jest buforem wielokrotnego użytku: **bez `clear()` komendy sumują się**
    /// z klatki na klatkę, przez co renderer rysuje coraz więcej i aplikacja zwalnia
    /// z każdą sekundą. Platforma czyści scenę przed każdym wywołaniem domknięcia;
    /// ten test pilnuje samego kontraktu, na którym to polega.
    #[test]
    fn reused_scene_accumulates_until_cleared() {
        let mut s = Scene::new();
        let rect = Rect::from_xywh(0.0, 0.0, 4.0, 4.0);
        for _ in 0..100 {
            s.fill_rect(rect, Color::BLACK);
        }
        assert_eq!(s.len(), 100, "bez clear() komendy sumują się");
        s.clear();
        assert!(s.is_empty(), "clear() opróżnia scenę do reużycia");
    }
}