🔒 Repository is read-only – file editing is disabled.
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");
}
}