🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
//! Renderowanie: budowa listy elementów i rysowanie do wyjścia.
//!
//! PO CO osobno od `state.rs`: zgodnie z wymaganiem modularności rozdzielamy
//! "co jest w świecie" (stan) od "jak to narysować" (render).
//!
//! Typy renderowania są **specjalizowane do `GlesRenderer`**. Powód: pasek SSD
//! i menu rysujemy własnym pixel shaderem (`PixelShaderElement`), a to API
//! Smithaya działa tylko z konkretnym rendererem. Używamy `GlesRenderer` w obu
//! backendach (winit, udev), więc nie tracimy na tym nic, a zyskujemy prostsze typy.
use std::{collections::HashMap, fmt, sync::Mutex, sync::OnceLock, time::Duration};
use smithay::{
backend::renderer::{
damage::{Error as OutputDamageTrackerError, OutputDamageTracker, RenderOutputResult},
element::{
memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement},
solid::SolidColorRenderElement,
surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement},
Id, Kind,
},
gles::{
element::PixelShaderElement, GlesError, GlesPixelProgram, GlesRenderer, GlesTarget,
Uniform, UniformName, UniformType,
},
utils::CommitCounter,
Color32F,
},
desktop::{space::SpaceRenderElements, Space},
input::pointer::CursorImageStatus,
output::Output,
render_elements,
utils::{IsAlive, Logical, Physical, Point, Rectangle, Scale, Size},
};
use crate::{
shaders::ROUNDED_CORNERS_FRAG,
state::DndIcon,
theme,
window::{PaganWindow, WindowRenderElement},
};
// Kolor tła pulpitu i kolory awaryjnego kursora pochodzą z motywu (`theme`).
// Domyślny motyw odpowiada ciemnemu wyglądowi PaganDE; użytkownik zmienia go
// w `~/.config/pagan/theme.conf` (przeładowanie natychmiastowe).
// Elementy "nakładkowe" rysowane nad przestrzenią okien (u nas: kursor).
//
// Celowo specjalizowane do `GlesRenderer`, żeby dały się mieszać w jednej liście
// z elementami okien (które zawierają `PixelShaderElement`).
// * `Surface` — kursor ustawiony przez klienta (`wl_pointer.set_cursor`),
// * `Memory` — kursor z motywu XCursor (nasz domyślny),
// * `Pointer` — zapasowy prostokąt, gdy nie ma motywu.
render_elements! {
pub CustomRenderElements<=GlesRenderer>;
Surface=WaylandSurfaceRenderElement<GlesRenderer>,
Memory=MemoryRenderBufferRenderElement<GlesRenderer>,
Pointer=SolidColorRenderElement,
Preview=PixelShaderElement,
}
impl fmt::Debug for CustomRenderElements {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(),
Self::Memory(arg0) => f.debug_tuple("Memory").field(arg0).finish(),
Self::Pointer(arg0) => f.debug_tuple("Pointer").field(arg0).finish(),
Self::Preview(arg0) => f.debug_tuple("Preview").field(arg0).finish(),
Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
}
}
}
// Pełna lista elementów jednej klatki: przestrzeń okien + nakładki.
render_elements! {
pub OutputRenderElements<=GlesRenderer>;
Space=SpaceRenderElements<GlesRenderer, WindowRenderElement>,
Custom=CustomRenderElements,
}
impl fmt::Debug for OutputRenderElements {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Space(arg0) => f.debug_tuple("Space").field(arg0).finish(),
Self::Custom(arg0) => f.debug_tuple("Custom").field(arg0).finish(),
Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
}
}
}
/// Kursor rysowany przez kompozytor.
///
/// Trzy źródła, w kolejności ważności:
/// 1. kursor ustawiony przez klienta (`CursorImageStatus::Surface`),
/// 2. nazwany kursor z motywu XCursor (`CursorImageStatus::Named`),
/// 3. zapasowy prostokąt (gdy motywu nie ma — np. brak `libXcursor`/plików).
///
/// Motyw ładujemy raz (globalny `cursor::theme()`), a wbudowany obraz buforujemy
/// per nazwa ikony — inaczej parsowalibyśmy plik kursora w każdej klatce.
pub struct PointerElement {
location: Point<i32, Physical>,
visible: bool,
status: CursorImageStatus,
/// (nazwa ikony, bufor, hotspot) ostatnio przygotowanego kursora z motywu.
cached: Option<(String, MemoryRenderBuffer, (i32, i32))>,
}
impl Default for PointerElement {
fn default() -> Self {
Self {
location: Point::from((0, 0)),
visible: true,
status: CursorImageStatus::default_named(),
cached: None,
}
}
}
impl PointerElement {
/// Aktualizuje pozycję (fizyczną) i widoczność kursora przed renderem.
pub fn update(&mut self, location: Point<i32, Physical>, visible: bool) {
self.location = location;
self.visible = visible;
}
/// Ustawia żądany kształt kursora (pochodzi z `SeatHandler::cursor_image`).
pub fn set_status(&mut self, status: CursorImageStatus) {
self.status = status;
}
/// Buduje elementy kursora. Wymaga renderera, bo import tekstury z pamięci
/// (`MemoryRenderBufferRenderElement`) jest operacją renderera.
pub fn render_elements(&mut self, renderer: &mut GlesRenderer) -> Vec<CustomRenderElements> {
if !self.visible {
return Vec::new();
}
match self.status.clone() {
CursorImageStatus::Hidden => Vec::new(),
CursorImageStatus::Named(icon) => {
let name = icon.name().to_string();
let needs_reload = self
.cached
.as_ref()
.map(|(cached, _, _)| cached != &name)
.unwrap_or(true);
if needs_reload {
self.cached = crate::cursor::theme()
.buffer(icon, 1, Duration::ZERO)
.map(|(buffer, hotspot)| (name.clone(), buffer, hotspot));
}
if let Some((_, buffer, (hot_x, hot_y))) = &self.cached {
// Odejmujemy hotspot, żeby czubek strzałki trafiał w kursor,
// a nie lewy górny róg obrazka.
let loc = Point::<i32, Physical>::from((
self.location.x - hot_x,
self.location.y - hot_y,
));
if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
renderer,
loc.to_f64(),
buffer,
None,
None,
None,
Kind::Cursor,
) {
return vec![CustomRenderElements::Memory(element)];
}
}
// Brak motywu (albo błąd importu) → czytelny prostokąt zapasowy.
self.fallback_rectangle()
}
CursorImageStatus::Surface(surface) => {
let elements = render_elements_from_surface_tree(
renderer,
&surface,
self.location,
Scale::from(1.0),
1.0,
Kind::Cursor,
);
elements
.into_iter()
.map(CustomRenderElements::Surface)
.collect()
}
}
}
/// Awaryjny kursor: dwuwarstwowy prostokąt (ciemna obwódka + jasny środek),
/// czytelny na każdym tle. Używany tylko, gdy nie ma kursora z motywu.
fn fallback_rectangle(&self) -> Vec<CustomRenderElements> {
let loc = self.location;
let theme = theme::current();
vec![
CustomRenderElements::Pointer(SolidColorRenderElement::new(
Id::new(),
Rectangle::new(loc, Size::from((12, 18))),
CommitCounter::default(),
theme.cursor_border,
Kind::Cursor,
)),
CustomRenderElements::Pointer(SolidColorRenderElement::new(
Id::new(),
Rectangle::new(Point::from((loc.x + 2, loc.y + 2)), Size::from((8, 13))),
CommitCounter::default(),
theme.cursor_fill,
Kind::Cursor,
)),
]
}
}
/// Składa elementy klatki: okna z przestrzeni + nakładki (kursor, DnD, duch snapu).
pub fn output_elements(
space: &Space<PaganWindow>,
output: &Output,
pointer: Option<&mut PointerElement>,
dnd: Option<&DndIcon>,
preview: Option<Rectangle<i32, Logical>>,
pointer_location: Point<f64, Logical>,
renderer: &mut GlesRenderer,
) -> (Vec<OutputRenderElements>, Color32F) {
let space_elements = smithay::desktop::space::space_render_elements::<_, PaganWindow, _>(
renderer,
[space],
output,
1.0,
)
.expect("output bez ustawionego trybu? (patrz: output.change_current_state)");
// KOLEJNOŚĆ JEST KRYTYCZNA: `render_output` oczekuje elementów
// OD PRZODU DO TYŁU (indeks 0 = na wierzchu), a rysuje listę od końca.
// Smithay robi tak samo w `space::render_output`: najpierw elementy
// niestandardowe (kursor, DnD), dopiero potem zawartość przestrzeni.
// Gdyby kursor trafił na koniec listy, wylądowałby POD oknami.
let mut elements: Vec<OutputRenderElements> = Vec::with_capacity(space_elements.len() + 2);
// 1) Kursor — absolutnie na wierzchu (indeks 0).
if let Some(pointer) = pointer {
elements.extend(
pointer
.render_elements(renderer)
.into_iter()
.map(OutputRenderElements::Custom),
);
}
// 2) Ikona przeciągania (drag & drop) — tuż POD kursorem, ale nad oknami.
// Bez tego użytkownik nie widzi, co przeciąga.
if let Some(icon) = dnd.filter(|icon| icon.surface.alive()) {
// Pozycja ikony = pozycja kursora + przesunięcie narzucone przez klienta
// (`buffer_delta`), żeby ikona „trzymała się” tego samego punktu.
let position = (pointer_location + icon.offset.to_f64())
.to_physical(Scale::from(1.0))
.to_i32_round();
elements.extend(
render_elements_from_surface_tree(
renderer,
&icon.surface,
position,
Scale::from(1.0),
1.0,
Kind::Unspecified,
)
.into_iter()
.map(|element| OutputRenderElements::Custom(CustomRenderElements::Surface(element))),
);
}
// 3) „Duch” doklejania (półprzezroczysty, zaokrąglony podgląd geometrii) —
// nad oknami, ale pod kursorem. Pokazujemy go, zanim użytkownik puści
// przycisk, żeby wiedział, gdzie okno wyląduje.
if let Some(geometry) = preview {
if let Some(element) = preview_element(renderer, geometry) {
elements.push(OutputRenderElements::Custom(CustomRenderElements::Preview(
element,
)));
}
}
// 4) Zawartość przestrzeni (okna razem z ich dekoracjami SSD) — na spodzie.
elements.extend(space_elements.into_iter().map(OutputRenderElements::Space));
(elements, theme::current().background)
}
/// Cache programów „ducha” (shader zaokrągleń), kluczowany kontekstem EGL.
///
/// PO CO klucz po kontekście: program GL żyje w konkretnym kontekście — ten sam
/// powód co przy shaderze paska SSD (patrz `window.rs`).
fn preview_programs() -> &'static Mutex<HashMap<usize, GlesPixelProgram>> {
static CACHE: OnceLock<Mutex<HashMap<usize, GlesPixelProgram>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
/// Kompiluje (raz na kontekst) i zwraca program zaokrąglonego prostokąta.
fn preview_program(renderer: &mut GlesRenderer) -> Option<GlesPixelProgram> {
let key = renderer.egl_context().get_context_handle() as usize;
if let Some(program) = preview_programs()
.lock()
.expect("cache shaderów zatruty")
.get(&key)
{
return Some(program.clone());
}
let uniforms = [
UniformName::new("radii", UniformType::_4f),
UniformName::new("fill_color", UniformType::_4f),
UniformName::new("fill_color2", UniformType::_4f),
UniformName::new("gradient_mix", UniformType::_1f),
UniformName::new("softness", UniformType::_1f),
UniformName::new("rect_size", UniformType::_2f),
UniformName::new("angle", UniformType::_1f),
];
match renderer.compile_custom_pixel_shader(ROUNDED_CORNERS_FRAG, &uniforms) {
Ok(program) => {
preview_programs()
.lock()
.expect("cache shaderów zatruty")
.insert(key, program.clone());
Some(program)
}
Err(err) => {
tracing::error!(?err, "nie udało się skompilować shadera ducha snapu");
None
}
}
}
/// Buduje element zaokrąglonego prostokąta o zadanym kolorze i promieniu
/// (promień dotyczy wszystkich czterech rogów).
///
/// Wspólny dla „ducha” doklejania, panelu menu i glifów dekoracji.
pub fn rounded_rect_element(
renderer: &mut GlesRenderer,
area: Rectangle<i32, Logical>,
color: Color32F,
radius: f32,
) -> Option<PixelShaderElement> {
shape_element(
renderer,
area,
area.size,
0.0,
(radius, radius, radius, radius),
color,
color,
false,
)
}
/// Buduje element prostokąta zaokrąglonego TYLKO u góry (dolne rogi ostre).
///
/// PO CO: belka dekoracji SSD musi mieć proste dolne rogi — stykają się one
/// z treścią okna i zaokrąglenie zostawiłoby prześwit na pulpit.
pub fn top_rounded_rect_element(
renderer: &mut GlesRenderer,
area: Rectangle<i32, Logical>,
color: Color32F,
radius: f32,
) -> Option<PixelShaderElement> {
shape_element(
renderer,
area,
area.size,
0.0,
(radius, radius, 0.0, 0.0),
color,
color,
false,
)
}
/// Wspólny konstruktor elementu kształtu — jedno miejsce na uniformy shadera.
#[allow(clippy::too_many_arguments)]
fn shape_element(
renderer: &mut GlesRenderer,
area: Rectangle<i32, Logical>,
shape_size: Size<i32, Logical>,
angle: f32,
radii: (f32, f32, f32, f32),
top: Color32F,
bottom: Color32F,
gradient: bool,
) -> Option<PixelShaderElement> {
if area.size.w <= 0 || area.size.h <= 0 || shape_size.w <= 0 || shape_size.h <= 0 {
return None;
}
let program = preview_program(renderer)?;
let uniforms = vec![
Uniform::new("radii", (radii.0, radii.1, radii.2, radii.3)),
Uniform::new("fill_color", (top.r(), top.g(), top.b(), top.a())),
Uniform::new(
"fill_color2",
(bottom.r(), bottom.g(), bottom.b(), bottom.a()),
),
Uniform::new("gradient_mix", if gradient { 1.0f32 } else { 0.0f32 }),
// ~1 px wygładzenia krawędzi (antyaliasing kształtu).
Uniform::new("softness", 1.0f32),
Uniform::new("rect_size", (shape_size.w as f32, shape_size.h as f32)),
Uniform::new("angle", angle),
];
Some(PixelShaderElement::new(
program,
area,
None,
1.0,
uniforms,
Kind::Unspecified,
))
}
/// Buduje element „ducha” — zaokrąglony, półprzezroczysty prostokąt z motywu.
fn preview_element(
renderer: &mut GlesRenderer,
geometry: Rectangle<i32, Logical>,
) -> Option<PixelShaderElement> {
let theme = theme::current();
rounded_rect_element(
renderer,
geometry,
theme.snap_preview,
theme.snap_preview_radius,
)
}
/// Kompiluje shadery „na zimno” przy starcie.
///
/// PO CO: błąd GLSL ma wylecieć od razu (w logu startowym), a nie dopiero przy
/// pierwszym przeciąganiu okna; przy okazji pierwsza klatka nie haczy na kompilacji.
pub fn warm_up_shaders(renderer: &mut GlesRenderer) {
match preview_program(renderer) {
Some(_) => tracing::info!("shader ducha snapu (rounded_corners) skompilowany"),
None => tracing::warn!(
"shader ducha snapu NIE skompilował się — duch doklejania nie będzie rysowany"
),
}
}
/// Renderuje jedną klatkę wyjścia i zwraca wynik z listą uszkodzonych regionów.
pub fn render_output<'d>(
output: &Output,
space: &Space<PaganWindow>,
pointer: Option<&mut PointerElement>,
dnd: Option<&DndIcon>,
preview: Option<Rectangle<i32, Logical>>,
pointer_location: Point<f64, Logical>,
renderer: &mut GlesRenderer,
framebuffer: &mut GlesTarget<'_>,
damage_tracker: &'d mut OutputDamageTracker,
age: usize,
) -> Result<RenderOutputResult<'d>, OutputDamageTrackerError<GlesError>> {
let (elements, clear_color) = output_elements(
space,
output,
pointer,
dnd,
preview,
pointer_location,
renderer,
);
damage_tracker.render_output(renderer, framebuffer, age, &elements, clear_color)
}
/// Zamienia pozycję logiczną kursora na fizyczną (do rysowania).
pub fn pointer_physical(location: Point<f64, Logical>) -> Point<i32, Physical> {
location.to_physical(Scale::from(1.0)).to_i32_round()
}