//! 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, Memory=MemoryRenderBufferRenderElement, 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, 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, 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, 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 { 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::::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 { 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, output: &Output, pointer: Option<&mut PointerElement>, dnd: Option<&DndIcon>, preview: Option>, pointer_location: Point, renderer: &mut GlesRenderer, ) -> (Vec, 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 = 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> { static CACHE: OnceLock>> = 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 { 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, color: Color32F, radius: f32, ) -> Option { 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, color: Color32F, radius: f32, ) -> Option { 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, shape_size: Size, angle: f32, radii: (f32, f32, f32, f32), top: Color32F, bottom: Color32F, gradient: bool, ) -> Option { 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, ) -> Option { 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, pointer: Option<&mut PointerElement>, dnd: Option<&DndIcon>, preview: Option>, pointer_location: Point, renderer: &mut GlesRenderer, framebuffer: &mut GlesTarget<'_>, damage_tracker: &'d mut OutputDamageTracker, age: usize, ) -> Result, OutputDamageTrackerError> { 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) -> Point { location.to_physical(Scale::from(1.0)).to_i32_round() }