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

luna/de/selene/src/decoration.rs main

210 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
//! `zxdg_decoration_manager_v1` — negocjacja dekoracji okien.
//!
//! Domyślnie **my** rysujemy pasek tytułu (spójny wygląd całej Luny): na prośbę
//! o `client_side` odpowiadamy `server_side`, więc GTK3 nie rysuje własnego CSD.
//! Wyjątki: `LUNA_CLIENT_DECORATIONS=1` (klient decyduje) — przydatne, gdy jakaś
//! aplikacja naprawdę wymaga własnych dekoracji.

use wayland_protocols::xdg::decoration::zv1::server::{
    zxdg_decoration_manager_v1 as decoration_manager,
    zxdg_toplevel_decoration_v1 as toplevel_decoration,
};
use wayland_protocols::xdg::shell::server::xdg_toplevel;
use wayland_server::{
    Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
};

use crate::shell::XdgToplevelData;
use crate::state::State;
use crate::sync::LockExt;

use libluna_core::Color;
use libluna_render::ImageData;

/// Uchwyt obiektu dekoracji okna.
pub type ToplevelDecoration = toplevel_decoration::ZxdgToplevelDecorationV1;

/// Negocjowany tryb dekoracji okna.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DecoMode {
    /// Dekoracje rysuje serwer (nasz pasek tytułu).
    Server,
    /// Dekoracje rysuje klient (CSD).
    Client,
}

/// Dane obiektu dekoracji — trzymamy powiązany `xdg_toplevel`, by móc odczytać
/// i zapisać tryb w [`XdgToplevelData`].
pub struct DecoData {
    pub toplevel: xdg_toplevel::XdgToplevel,
}

/// Dobiera kolor paska SSD do górnej części treści okna.
///
/// Próbkujemy pierwsze ~28 px bufora klienta (tam zwykle jest pasek narzędzi/header
/// aplikacji), pomijając piksele przezroczyste (cienie CSD). Zwracamy parę
/// `(tło paska, kolor tekstu/ikon)` z automatycznym kontrastem.
pub fn tint_from_image(img: &ImageData) -> Option<(Color, Color)> {
    if img.width == 0 || img.height == 0 {
        return None;
    }
    let expected = img.width as usize * img.height as usize * 4;
    if img.rgba.len() < expected {
        return None;
    }
    let y1 = img.height.min(28);
    let step_x = (img.width / 48).max(1);
    let (mut r, mut g, mut b, mut n) = (0u64, 0u64, 0u64, 0u64);
    for y in 0..y1 {
        let mut x = 0;
        while x < img.width {
            let i = ((y * img.width + x) * 4) as usize;
            if img.rgba[i + 3] > 200 {
                r += img.rgba[i] as u64;
                g += img.rgba[i + 1] as u64;
                b += img.rgba[i + 2] as u64;
                n += 1;
            }
            x += step_x;
        }
    }
    if n < 8 {
        return None;
    }
    let bg = Color::from_rgba8((r / n) as u8, (g / n) as u8, (b / n) as u8, 0xff);
    // Percepcyjna luminancja — na jasnym tle dajemy ciemny tekst, na ciemnym biały.
    let lum = 0.2126 * bg.r + 0.7152 * bg.g + 0.0722 * bg.b;
    let fg = if lum > 0.60 {
        Color::from_rgba8(0x1c, 0x1c, 0x1c, 0xff)
    } else {
        Color::WHITE
    };
    Some((bg, fg))
}

impl GlobalDispatch<decoration_manager::ZxdgDecorationManagerV1, ()> for State {
    fn bind(
        _state: &mut Self,
        _handle: &DisplayHandle,
        _client: &Client,
        resource: New<decoration_manager::ZxdgDecorationManagerV1>,
        _data: &(),
        data_init: &mut DataInit<'_, Self>,
    ) {
        data_init.init(resource, ());
    }
}

impl Dispatch<decoration_manager::ZxdgDecorationManagerV1, ()> for State {
    fn request(
        _state: &mut Self,
        _client: &Client,
        _resource: &decoration_manager::ZxdgDecorationManagerV1,
        request: decoration_manager::Request,
        _data: &(),
        _handle: &DisplayHandle,
        data_init: &mut DataInit<'_, Self>,
    ) {
        if let decoration_manager::Request::GetToplevelDecoration { id, toplevel } = request {
            let deco = data_init.init(
                id,
                DecoData {
                    toplevel: toplevel.clone(),
                },
            );
            if let Some(td) = toplevel.data::<XdgToplevelData>() {
                *td.decoration.lock_or_recover() = Some(deco);
            }
        }
    }
}

impl Dispatch<toplevel_decoration::ZxdgToplevelDecorationV1, DecoData> for State {
    fn request(
        _state: &mut Self,
        _client: &Client,
        resource: &toplevel_decoration::ZxdgToplevelDecorationV1,
        request: toplevel_decoration::Request,
        data: &DecoData,
        _handle: &DisplayHandle,
        _data_init: &mut DataInit<'_, Self>,
    ) {
        let Some(td) = data.toplevel.data::<XdgToplevelData>() else {
            return;
        };
        match request {
            toplevel_decoration::Request::SetMode { mode } => {
                // **Zawsze dekorujemy sami.** GTK3 (thunar, xfce4-terminal, gedit…) domyślnie
                // prosi o `client_side`; gdybyśmy się zgodzili, każde okno miałoby inny
                // pasek tytułu (albo żaden). Odpowiadamy `server_side` — GTK wtedy wyłącza
                // własne CSD i korzysta z naszego paska, spójnego z resztą Luny.
                // `LUNA_CLIENT_DECORATIONS=1` przywraca dawną politykę (klient decyduje).
                let honor = std::env::var("LUNA_CLIENT_DECORATIONS").is_ok_and(|v| v != "0");
                let requested = if honor {
                    match mode {
                        WEnum::Value(toplevel_decoration::Mode::ServerSide) => DecoMode::Server,
                        _ => DecoMode::Client,
                    }
                } else {
                    DecoMode::Server
                };
                *td.deco.lock_or_recover() = Some(requested);
                let reply = match requested {
                    DecoMode::Server => toplevel_decoration::Mode::ServerSide,
                    DecoMode::Client => toplevel_decoration::Mode::ClientSide,
                };
                let _ = resource.send_event(toplevel_decoration::Event::Configure {
                    mode: WEnum::Value(reply),
                });
            }
            // Klient oddaje decyzję kompozytorowi — domyślnie dekorujemy my.
            toplevel_decoration::Request::UnsetMode => {
                *td.deco.lock_or_recover() = Some(DecoMode::Server);
                let _ = resource.send_event(toplevel_decoration::Event::Configure {
                    mode: WEnum::Value(toplevel_decoration::Mode::ServerSide),
                });
            }
            toplevel_decoration::Request::Destroy => {
                *td.deco.lock_or_recover() = None;
                *td.decoration.lock_or_recover() = None;
            }
            _ => {}
        }
    }
}

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

    fn image(color: [u8; 3], alpha: u8) -> ImageData {
        let (w, h) = (64u32, 64u32);
        let mut rgba = vec![0u8; (w * h * 4) as usize];
        for y in 0..20 {
            for x in 0..w {
                let i = ((y * w + x) * 4) as usize;
                rgba[i] = color[0];
                rgba[i + 1] = color[1];
                rgba[i + 2] = color[2];
                rgba[i + 3] = alpha;
            }
        }
        ImageData::new(w, h, rgba)
    }

    #[test]
    fn tint_takes_top_strip_and_picks_contrast() {
        let (bg, fg) = tint_from_image(&image([30, 40, 50], 255)).expect("ciemny pasek");
        assert!((bg.r - 30.0 / 255.0).abs() < 0.01);
        assert_eq!(fg, Color::WHITE);

        let (bg, fg) = tint_from_image(&image([240, 240, 240], 255)).expect("jasny pasek");
        assert!(bg.r > 0.9);
        assert!(fg.r < 0.2, "na jasnym tle tekst musi być ciemny");
    }

    #[test]
    fn transparent_pixels_do_not_tint() {
        assert!(tint_from_image(&image([200, 50, 50], 0)).is_none());
    }
}