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

PaganDE/pagan-compositor/src/render.rs main

476 linii Raw ← Powrót
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()
}