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

PaganDE/pagan-panel/src/main.rs main

921 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921
//! PaganDE: panel + pasek zadań.
//!
//! PO CO osobny program: panel to zwykły klient Waylanda, ale **nie** okno —
//! to powierzchnia warstwy (`wlr-layer-shell`), która rezerwuje pas ekranu
//! (`exclusive_zone`), więc okna nie wchodzą pod pasek.
//!
//! Co robi:
//! * lista okien z kompozytora (`wlr-foreign-toplevel-management`) → kafelki,
//! * kliknięcie kafelka aktywuje okno (i przywraca zminimalizowane),
//!   środkowy przycisk zamyka okno,
//! * przycisk „Pagan” uruchamia terminal (prosty launcher),
//! * zegar po prawej (odświeżany timerem calloop).
//!
//! Wygląd: ciemny, półprzezroczysty pasek z zaokrąglonymi DOLNYMI rogami i akcentem
//! `#4ea1ff` — spójny z motywem kompozytora. Rysujemy Cairo do bufora SHM, dzieląc
//! `ShmBuffer` z `pagan-toolkit`.

use std::{process::Command, time::Duration};

use cairo::{Context, FontSlant, FontWeight, Format, ImageSurface};
use calloop::{timer::TimeoutAction, EventLoop, LoopHandle};
use calloop_wayland_source::WaylandSource;
use chrono::Local;
use memmap2::MmapMut;
use tracing::{error, info, warn};

use wayland_client::{
    globals::{registry_queue_init, GlobalListContents},
    protocol::{
        wl_buffer, wl_callback, wl_compositor, wl_output, wl_pointer, wl_registry, wl_seat, wl_shm,
        wl_shm_pool, wl_surface,
    },
    Connection, Dispatch, Proxy, QueueHandle,
};
use wayland_protocols_wlr::{
    foreign_toplevel::v1::client::{
        zwlr_foreign_toplevel_handle_v1 as ft_handle,
        zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
        zwlr_foreign_toplevel_manager_v1 as ft_manager,
        zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1,
    },
    layer_shell::v1::client::{
        zwlr_layer_shell_v1::{self as layer_shell, Layer, ZwlrLayerShellV1},
        zwlr_layer_surface_v1::{self as layer_surface, Anchor, ZwlrLayerSurfaceV1},
    },
};

use pagan_toolkit::shm::ShmBuffer;

// --- Geometria paska ---------------------------------------------------------

/// Wysokość panelu (px logiczne) — zarazem `exclusive_zone`.
const BAR_HEIGHT: i32 = 36;
/// Promień zaokrąglenia dolnych rogów.
const BAR_RADIUS: f64 = 12.0;
/// Margines boczny zawartości.
const PAD: f64 = 10.0;
/// Odstęp między elementami.
const GAP: f64 = 8.0;
/// Wysokość „pigułek” (kafelków/przycisków).
const PILL_H: f64 = 24.0;
/// Szerokość przycisku launchera terminala.
const LAUNCHER_W: f64 = 92.0;
/// Szerokość przycisku launchera aplikacji testowej (PaganDE demo).
const DEMO_W: f64 = 74.0;
/// Maksymalna szerokość kafelka okna.
const TASK_MAX_W: f64 = 190.0;
/// Minimalna szerokość kafelka okna.
const TASK_MIN_W: f64 = 96.0;

// --- Kolory (spójne z motywem kompozytora) -----------------------------------

const BG: (f64, f64, f64, f64) = (0.090, 0.098, 0.125, 0.94);
const ACCENT: (f64, f64, f64) = (0.306, 0.631, 1.0);
const TEXT: (f64, f64, f64) = (0.87, 0.89, 0.93);
const TEXT_DIM: (f64, f64, f64) = (0.66, 0.69, 0.75);
const TASK_BG: (f64, f64, f64, f64) = (1.0, 1.0, 1.0, 0.07);
const TASK_HOVER_BG: (f64, f64, f64, f64) = (1.0, 1.0, 1.0, 0.13);
const TASK_ACTIVE_BG: (f64, f64, f64, f64) = (0.306, 0.631, 1.0, 0.22);

/// Jedno okno widziane przez protokół listy okien.
struct Task {
    handle: ZwlrForeignToplevelHandleV1,
    title: String,
    activated: bool,
    minimized: bool,
    /// Położenie i szerokość kafelka (liczone przy rysowaniu, używane do trafień).
    layout: (f64, f64),
}

/// Stan panelu.
struct Panel {
    connection: Connection,
    qh: QueueHandle<Panel>,

    // Trzymane, by obiekty protokołu żyły tak długo jak panel (upuszczenie
    // zniszczyłoby `wl_surface`/warstwę). Nie czytamy ich pól.
    #[allow(dead_code)]
    compositor: wl_compositor::WlCompositor,
    #[allow(dead_code)]
    layer_shell: ZwlrLayerShellV1,
    #[allow(dead_code)]
    manager: ZwlrForeignToplevelManagerV1,
    shm: wl_shm::WlShm,
    #[allow(dead_code)]
    layer_surface: ZwlrLayerSurfaceV1,
    surface: wl_surface::WlSurface,
    seat: wl_seat::WlSeat,
    pointer_handle: Option<wl_pointer::WlPointer>,
    /// Callback klatki — trzymany, by kompozytor nie wysyłał zdarzeń do „martwego” obiektu.
    frame_cb: Option<wl_callback::WlCallback>,

    /// Rozmiar powierzchni z ostatniego `configure` (px logiczne).
    size: (i32, i32),
    tasks: Vec<Task>,
    /// Indeks kafelka pod kursorem.
    hover: Option<usize>,
    /// Czy kursor jest nad przyciskiem launchera (terminal).
    hover_launcher: bool,
    /// Czy kursor jest nad przyciskiem launchera aplikacji testowej.
    hover_demo: bool,
    /// Ostatnia pozycja kursora w układzie powierzchni.
    pointer_pos: (f64, f64),
    launcher_rect: (f64, f64, f64, f64),
    demo_rect: (f64, f64, f64, f64),
    clock: String,
    dirty: bool,
    closed: bool,

    /// Bufory czekające na zwolnienie (podmieniane przy każdej klatce).
    current: Option<ShmBuffer>,
    stale: Vec<ShmBuffer>,
}

impl Panel {
    fn update_clock(&mut self) {
        self.clock = Local::now().format("%H:%M").to_string();
    }

    /// Rysuje pasek do bufora SHM i wysyła go kompozytorowi.
    fn redraw(&mut self) {
        let (width, height) = self.size;
        if width <= 0 || height <= 0 || !self.dirty {
            return;
        }

        let mut buffer = match ShmBuffer::new(&self.shm, &self.qh, width, height) {
            Ok(buffer) => buffer,
            Err(err) => {
                error!(%err, "nie udało się zaalokować bufora panelu");
                return;
            }
        };

        self.paint(&mut buffer.mmap, width, height);

        self.surface.attach(Some(&buffer.buffer), 0, 0);
        self.surface.damage(0, 0, width, height);
        self.surface.commit();
        self.frame_cb = Some(self.surface.frame(&self.qh, ()));
        let _ = self.connection.flush();

        if let Some(previous) = self.current.replace(buffer) {
            self.stale.push(previous);
        }
        self.dirty = false;
    }

    /// Cała grafika paska (Cairo).
    fn paint(&mut self, mmap: &mut MmapMut, width: i32, height: i32) {
        let stride = width * 4;
        // Powierzchnia Cairo wskazuje wprost na pamięć bufora SHM (jak w toolkicie);
        // `flush()` na końcu wypycha piksele.
        let surface = unsafe {
            ImageSurface::create_for_data_unsafe(
                mmap.as_mut_ptr(),
                Format::ARgb32,
                width,
                height,
                stride,
            )
        }
        .expect("powierzchnia Cairo na buforze SHM");
        let ctx = Context::new(&surface).expect("kontekst Cairo");

        // Tło z zaokrąglonymi dolnymi rogami.
        ctx.set_source_rgba(BG.0, BG.1, BG.2, BG.3);
        bar_path(&ctx, width as f64, height as f64, BAR_RADIUS);
        ctx.fill().ok();

        // Akcentowa linia pod paskiem.
        ctx.set_source_rgba(ACCENT.0, ACCENT.1, ACCENT.2, 0.55);
        ctx.set_line_width(1.0);
        ctx.move_to(0.0, height as f64 - 0.5);
        ctx.line_to(width as f64, height as f64 - 0.5);
        ctx.stroke().ok();

        ctx.select_font_face("Sans", FontSlant::Normal, FontWeight::Normal);
        ctx.set_font_size(13.0);

        let pill_y = (height as f64 - PILL_H) / 2.0;

        // 1) Launcher: terminal („Pagan”) + aplikacja testowa („Demo”).
        //
        // PO CO dwa przyciski: pierwszy uruchamia terminal (codzienna praca),
        // drugi naszą aplikację testową, żeby jednym kliknięciem sprawdzić
        // dekoracje SSD i menu aplikacji.
        let launcher_bg = if self.hover_launcher {
            TASK_HOVER_BG
        } else {
            TASK_BG
        };
        draw_pill(&ctx, PAD, pill_y, LAUNCHER_W, PILL_H, launcher_bg);
        ctx.set_source_rgb(ACCENT.0, ACCENT.1, ACCENT.2);
        ctx.set_font_size(14.0);
        draw_centered_text(&ctx, "Pagan", PAD, pill_y, LAUNCHER_W, PILL_H);
        ctx.set_font_size(13.0);
        self.launcher_rect = (PAD, pill_y, LAUNCHER_W, PILL_H);

        let demo_x = PAD + LAUNCHER_W + GAP;
        let demo_bg = if self.hover_demo {
            TASK_HOVER_BG
        } else {
            TASK_BG
        };
        draw_pill(&ctx, demo_x, pill_y, DEMO_W, PILL_H, demo_bg);
        ctx.set_source_rgb(TEXT.0, TEXT.1, TEXT.2);
        draw_centered_text(&ctx, "Demo", demo_x, pill_y, DEMO_W, PILL_H);
        self.demo_rect = (demo_x, pill_y, DEMO_W, PILL_H);

        // 2) Kafelki okien.
        let clock_w = 64.0;
        let right_limit = width as f64 - PAD - clock_w - GAP * 2.0;
        let mut x = demo_x + DEMO_W + GAP * 2.0;

        for (index, task) in self.tasks.iter_mut().enumerate() {
            let label = truncate(&ctx, &task.title, TASK_MAX_W - 16.0);
            let pill_w = (text_width(&ctx, &label) + 18.0).clamp(TASK_MIN_W, TASK_MAX_W);
            if x + pill_w > right_limit {
                break; // brak miejsca — dalsze okna nie mieszczą się na pasku
            }
            task.layout = (x, pill_w);

            let hovered = self.hover == Some(index);
            let background = if task.activated {
                TASK_ACTIVE_BG
            } else if hovered {
                TASK_HOVER_BG
            } else {
                TASK_BG
            };
            draw_pill(&ctx, x, pill_y, pill_w, PILL_H, background);

            // Kropka stanu.
            let dot = if task.activated {
                (ACCENT.0, ACCENT.1, ACCENT.2, 1.0)
            } else if task.minimized {
                (0.60, 0.60, 0.65, 0.70)
            } else {
                (0.45, 0.47, 0.52, 1.0)
            };
            ctx.set_source_rgba(dot.0, dot.1, dot.2, dot.3);
            ctx.arc(
                x + 9.0,
                pill_y + PILL_H / 2.0,
                2.6,
                0.0,
                std::f64::consts::TAU,
            );
            ctx.fill().ok();

            if task.activated {
                ctx.set_source_rgb(TEXT.0, TEXT.1, TEXT.2);
            } else {
                ctx.set_source_rgb(TEXT_DIM.0, TEXT_DIM.1, TEXT_DIM.2);
            }
            draw_text(&ctx, &label, x + 16.0, pill_y + PILL_H / 2.0);

            x += pill_w + GAP;
        }

        // 3) Zegar.
        ctx.set_source_rgb(TEXT.0, TEXT.1, TEXT.2);
        let clock = self.clock.clone();
        let clock_x = width as f64 - PAD - text_width(&ctx, &clock);
        draw_text(&ctx, &clock, clock_x, pill_y + PILL_H / 2.0);

        surface.flush();
    }

    fn launcher_contains(&self, (px, py): (f64, f64)) -> bool {
        let (x, y, w, h) = self.launcher_rect;
        px >= x && px <= x + w && py >= y && py <= y + h
    }

    fn demo_contains(&self, (px, py): (f64, f64)) -> bool {
        let (x, y, w, h) = self.demo_rect;
        px >= x && px <= x + w && py >= y && py <= y + h
    }

    /// Kafelek pod danym punktem.
    fn task_at(&self, (px, py): (f64, f64)) -> Option<usize> {
        let pill_y = (self.size.1 as f64 - PILL_H) / 2.0;
        self.tasks.iter().position(|task| {
            let (x, w) = task.layout;
            px >= x && px <= x + w && py >= pill_y && py <= pill_y + PILL_H
        })
    }

    /// Przelicza podświetlenia i — jeśli się zmieniły — przerysowuje pasek.
    fn refresh_hover(&mut self) {
        let point = self.pointer_pos;
        let new_task = self.task_at(point);
        let new_launcher = self.launcher_contains(point);
        let new_demo = self.demo_contains(point);
        if new_task != self.hover
            || new_launcher != self.hover_launcher
            || new_demo != self.hover_demo
        {
            self.hover = new_task;
            self.hover_launcher = new_launcher;
            self.hover_demo = new_demo;
            self.dirty = true;
            self.redraw();
        }
    }

    fn activate_task(&self, index: usize) {
        if let Some(task) = self.tasks.get(index) {
            task.handle.activate(&self.seat);
        }
    }

    fn close_task(&self, index: usize) {
        if let Some(task) = self.tasks.get(index) {
            task.handle.close();
        }
    }

    /// Prosty launcher: uruchamia pierwszy dostępny terminal.
    fn launch_terminal(&self) {
        for candidate in [
            "foot",
            "pagan-terminal",
            "xfce4-terminal",
            "kitty",
            "alacritty",
            "xterm",
        ] {
            if which(candidate) {
                match spawn_in_session(candidate) {
                    Ok(_) => {
                        info!(terminal = candidate, "uruchomiono terminal");
                        return;
                    }
                    Err(err) => warn!(terminal = candidate, %err, "nie udało się uruchomić"),
                }
            }
        }
        warn!("nie znalazłem terminala (foot/kitty/xterm/…) — launcher nic nie zrobił");
    }

    /// Launcher aplikacji testowej PaganDE (nasze demo toolkitu).
    ///
    /// PO CO: jednym kliknięciem sprawdzamy dekoracje SSD, menu aplikacji
    /// i wspólny wygląd okna. Kolejność szukania:
    /// 1. `$PAGAN_DEMO_CMD` (jawne nadpisanie — najwygodniejsze w testach),
    /// 2. nazwy w `PATH` (instalacja systemowa),
    /// 3. układ deweloperski repozytorium (`pagan-toolkit/target/…/examples/demo`).
    fn launch_demo(&self) {
        if let Ok(command) = std::env::var("PAGAN_DEMO_CMD") {
            if !command.trim().is_empty() {
                match spawn_in_session(&command) {
                    Ok(_) => {
                        info!(command, "uruchomiono aplikację demo (PAGAN_DEMO_CMD)");
                        return;
                    }
                    Err(err) => warn!(command, %err, "PAGAN_DEMO_CMD nie uruchomił się"),
                }
            }
        }

        let mut candidates: Vec<std::path::PathBuf> = Vec::new();
        for name in ["pagan-demo", "pagan-toolkit-demo"] {
            if which(name) {
                candidates.push(name.into());
            }
        }
        // Układ deweloperski: <repo>/pagan-panel/target/debug/pagan-panel,
        // więc demo leży w <repo>/pagan-toolkit/target/debug/examples/demo.
        if let Some(exe_dir) = std::env::current_exe()
            .ok()
            .and_then(|exe| exe.parent().map(|parent| parent.to_path_buf()))
        {
            let repo = exe_dir.join("..").join("..").join("..");
            candidates.push(repo.join("pagan-toolkit/target/debug/examples/demo"));
            candidates.push(repo.join("pagan-toolkit/target/release/examples/demo"));
        }

        for candidate in candidates {
            if candidate.exists() {
                match spawn_in_session(&candidate.to_string_lossy()) {
                    Ok(_) => {
                        info!(path = %candidate.display(), "uruchomiono aplikację demo");
                        return;
                    }
                    Err(err) => {
                        warn!(path = %candidate.display(), %err, "nie udało się uruchomić demo")
                    }
                }
            }
        }
        warn!(
            "nie znalazłem aplikacji demo — ustaw PAGAN_DEMO_CMD na ścieżkę do \
             pagan-toolkit/target/debug/examples/demo"
        );
    }
}

// --- Pomocnicze rysowanie ----------------------------------------------------

/// Ścieżka paska: pełna szerokość u góry, zaokrąglone DOLNE rogi.
fn bar_path(ctx: &Context, width: f64, height: f64, radius: f64) {
    let r = radius.min(height);
    ctx.new_path();
    ctx.move_to(0.0, 0.0);
    ctx.line_to(width, 0.0);
    ctx.line_to(width, height - r);
    ctx.arc(width - r, height - r, r, 0.0, std::f64::consts::FRAC_PI_2);
    ctx.arc(
        r,
        height - r,
        r,
        std::f64::consts::FRAC_PI_2,
        std::f64::consts::PI,
    );
    ctx.close_path();
}

/// „Pigułka” (zaokrąglony prostokąt) wypełniona kolorem.
fn draw_pill(ctx: &Context, x: f64, y: f64, w: f64, h: f64, color: (f64, f64, f64, f64)) {
    let r = h / 2.0;
    ctx.set_source_rgba(color.0, color.1, color.2, color.3);
    ctx.new_path();
    ctx.move_to(x + r, y);
    ctx.line_to(x + w - r, y);
    ctx.arc(x + w - r, y + r, r, -std::f64::consts::FRAC_PI_2, 0.0);
    ctx.arc(x + w - r, y + h - r, r, 0.0, std::f64::consts::FRAC_PI_2);
    ctx.line_to(x + r, y + h);
    ctx.arc(
        x + r,
        y + h - r,
        r,
        std::f64::consts::FRAC_PI_2,
        std::f64::consts::PI,
    );
    ctx.arc(
        x + r,
        y + r,
        r,
        std::f64::consts::PI,
        std::f64::consts::PI * 1.5,
    );
    ctx.close_path();
    ctx.fill().ok();
}

/// Tekst wyrównany do bazowej linii tak, by środek liter był na `center_y`.
fn draw_text(ctx: &Context, text: &str, x: f64, center_y: f64) {
    let Ok(extents) = ctx.text_extents(text) else {
        return;
    };
    ctx.move_to(x, center_y - extents.y_bearing() / 2.0);
    ctx.show_text(text).ok();
}

/// Tekst wyśrodkowany w prostokącie.
fn draw_centered_text(ctx: &Context, text: &str, x: f64, y: f64, w: f64, h: f64) {
    let Ok(extents) = ctx.text_extents(text) else {
        return;
    };
    ctx.move_to(
        x + (w - extents.x_advance()) / 2.0,
        y + h / 2.0 - extents.y_bearing() / 2.0,
    );
    ctx.show_text(text).ok();
}

fn text_width(ctx: &Context, text: &str) -> f64 {
    ctx.text_extents(text).map(|e| e.x_advance()).unwrap_or(0.0)
}

/// Skraca tytuł wielokropkiem, aż zmieści się w `max_width`.
fn truncate(ctx: &Context, text: &str, max_width: f64) -> String {
    if text_width(ctx, text) <= max_width {
        return text.to_string();
    }
    let mut result = String::new();
    for character in text.chars() {
        let candidate = format!("{result}{character}…");
        if text_width(ctx, &candidate) > max_width {
            break;
        }
        result.push(character);
    }
    format!("{result}…")
}

/// Czy program jest w `PATH` (prosty launcher bez dodatkowych zależności).
fn which(name: &str) -> bool {
    std::env::var_os("PATH")
        .map(|paths| std::env::split_paths(&paths).any(|dir| dir.join(name).is_file()))
        .unwrap_or(false)
}

/// Uruchamia program w środowisku NASZEJ sesji (a nie sesji XFCE).
///
/// PO CO własne zmienne środowiska: w sesji XFCE ustawione jest
/// `XDG_SESSION_TYPE=x11` i `DISPLAY=:0.0`, więc GUI aplikacji domyślnie wybiera
/// X11 i ląduje na pulpicie XFCE — POZA naszym kompozytorem. Wtedy okno i jego
/// dekoracja nie mają ze sobą nic wspólnego.
///
/// Wybór backendu zależy od tego, czy aplikacja umie oddać dekoracje:
/// * **GTK → XWayland (`x11`)**. GTK3 na Waylandzie ZAWSZE rysuje własne CSD
///   i nie zna `zxdg_decoration_manager_v1`, więc nie da się na nim wymusić
///   naszego paska. Na X11 CSD nie istnieje — dekorację rysuje kompozytor,
///   czyli MY, spójnie z resztą środowiska. Nadpisz: `PAGAN_GDK_BACKEND=wayland`.
/// * **Qt → Wayland**. Qt zna `xdg-decoration` i honoruje wymuszony tryb
///   serwerowy, więc dostaje nasz pasek bez XWayland.
/// * Aplikacjom czysto X11 (bez GDK/Qt) dajemy `DISPLAY` NASZEGO XWayland.
fn spawn_in_session(command: &str) -> std::io::Result<std::process::Child> {
    let mut cmd = Command::new(command);
    cmd.env("XDG_SESSION_TYPE", "wayland");
    let gdk_backend = std::env::var("PAGAN_GDK_BACKEND").unwrap_or_else(|_| "x11".to_string());
    cmd.env("GDK_BACKEND", gdk_backend);
    cmd.env("QT_QPA_PLATFORM", "wayland");
    if let Some(display) = compositor_xdisplay() {
        cmd.env("DISPLAY", display);
    }
    cmd.spawn()
}

/// Czyta `DISPLAY` naszego XWayland opublikowany przez kompozytor.
fn compositor_xdisplay() -> Option<String> {
    let dir = std::env::var("XDG_RUNTIME_DIR").ok()?;
    std::fs::read_to_string(
        std::path::PathBuf::from(dir)
            .join("pagan")
            .join("xwayland-display"),
    )
    .ok()
    .map(|value| value.trim().to_string())
    .filter(|value| !value.is_empty())
}

// --- Dispatch Waylanda -------------------------------------------------------

impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for Panel {
    fn event(
        _state: &mut Self,
        _proxy: &wl_registry::WlRegistry,
        _event: wl_registry::Event,
        _data: &GlobalListContents,
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
    }
}

/// Puste implementacje dla obiektów, których zdarzeń nie potrzebujemy.
macro_rules! ignore_events {
    ($($iface:ty),* $(,)?) => {
        $(
            impl Dispatch<$iface, ()> for Panel {
                fn event(
                    _state: &mut Self,
                    _proxy: &$iface,
                    _event: <$iface as Proxy>::Event,
                    _data: &(),
                    _conn: &Connection,
                    _qh: &QueueHandle<Self>,
                ) {
                }
            }
        )*
    };
}

ignore_events!(
    wl_compositor::WlCompositor,
    wl_shm::WlShm,
    wl_shm_pool::WlShmPool,
    wl_buffer::WlBuffer,
    wl_output::WlOutput,
);

impl Dispatch<wl_callback::WlCallback, ()> for Panel {
    fn event(
        state: &mut Self,
        _proxy: &wl_callback::WlCallback,
        event: wl_callback::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        if let wl_callback::Event::Done { .. } = event {
            // Kompozytor oddał klatkę — bufory z poprzednich klatek są wolne.
            state.stale.clear();
            state.frame_cb = None;
        }
    }
}

impl Dispatch<wl_surface::WlSurface, ()> for Panel {
    fn event(
        _state: &mut Self,
        _proxy: &wl_surface::WlSurface,
        _event: wl_surface::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
    }
}

impl Dispatch<wl_seat::WlSeat, ()> for Panel {
    fn event(
        state: &mut Self,
        seat: &wl_seat::WlSeat,
        event: wl_seat::Event,
        _data: &(),
        _conn: &Connection,
        qh: &QueueHandle<Self>,
    ) {
        if let wl_seat::Event::Capabilities { capabilities: caps } = event {
            // `caps` to `WEnum` (może nieść nieznaną wartość od nowszego serwera).
            let has_pointer = caps
                .into_result()
                .map(|caps| caps.contains(wl_seat::Capability::Pointer))
                .unwrap_or(false);
            if has_pointer && state.pointer_handle.is_none() {
                state.pointer_handle = Some(seat.get_pointer(qh, ()));
            }
        }
    }
}

impl Dispatch<wl_pointer::WlPointer, ()> for Panel {
    fn event(
        state: &mut Self,
        _pointer: &wl_pointer::WlPointer,
        event: wl_pointer::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        match event {
            wl_pointer::Event::Enter {
                surface_x,
                surface_y,
                ..
            } => {
                state.pointer_pos = (surface_x, surface_y);
                state.refresh_hover();
            }
            wl_pointer::Event::Leave { .. } => {
                if state.hover.take().is_some() || state.hover_launcher {
                    state.hover_launcher = false;
                    state.dirty = true;
                    state.redraw();
                }
            }
            wl_pointer::Event::Motion {
                surface_x,
                surface_y,
                ..
            } => {
                state.pointer_pos = (surface_x, surface_y);
                state.refresh_hover();
            }
            wl_pointer::Event::Button {
                button,
                state: button_state,
                ..
            } => {
                // BTN_LEFT = 0x110, BTN_MIDDLE = 0x112 (linux/input-event-codes.h)
                let pressed = button_state
                    .into_result()
                    .map(|state| state == wl_pointer::ButtonState::Pressed)
                    .unwrap_or(false);
                if pressed {
                    let point = state.pointer_pos;
                    if state.launcher_contains(point) {
                        state.launch_terminal();
                    } else if state.demo_contains(point) {
                        state.launch_demo();
                    } else if let Some(index) = state.task_at(point) {
                        match button {
                            0x112 => state.close_task(index),
                            _ => state.activate_task(index),
                        }
                    }
                }
            }
            _ => {}
        }
    }
}

impl Dispatch<ZwlrLayerShellV1, ()> for Panel {
    fn event(
        _state: &mut Self,
        _proxy: &ZwlrLayerShellV1,
        _event: layer_shell::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
    }
}

impl Dispatch<ZwlrLayerSurfaceV1, ()> for Panel {
    fn event(
        state: &mut Self,
        layer_surface: &ZwlrLayerSurfaceV1,
        event: layer_surface::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        match event {
            layer_surface::Event::Configure {
                serial,
                width,
                height,
            } => {
                // Kompozytor mówi, jak duża ma być powierzchnia — trzeba potwierdzić.
                layer_surface.ack_configure(serial);
                state.size = (width as i32, height as i32);
                info!(width, height, "panel skonfigurowany");
                state.dirty = true;
                state.redraw();
            }
            layer_surface::Event::Closed => {
                info!("kompozytor zamknął powierzchnię panelu — kończę");
                state.closed = true;
            }
            _ => {}
        }
    }
}

impl Dispatch<ZwlrForeignToplevelManagerV1, ()> for Panel {
    // Zdarzenie `toplevel` tworzy u klienta nowy obiekt uchwytu — trzeba wskazać
    // jego typ, inaczej `wayland-client` panikuje.
    wayland_client::event_created_child!(Panel, ZwlrForeignToplevelManagerV1, [
        ft_manager::EVT_TOPLEVEL_OPCODE => (ZwlrForeignToplevelHandleV1, ())
    ]);

    fn event(
        state: &mut Self,
        _manager: &ZwlrForeignToplevelManagerV1,
        event: ft_manager::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        match event {
            ft_manager::Event::Toplevel { toplevel } => {
                state.tasks.push(Task {
                    handle: toplevel,
                    title: String::new(),
                    activated: false,
                    minimized: false,
                    layout: (0.0, 0.0),
                });
            }
            ft_manager::Event::Finished => info!("kompozytor zakończył listę okien"),
            _ => {}
        }
    }
}

impl Dispatch<ZwlrForeignToplevelHandleV1, ()> for Panel {
    fn event(
        state: &mut Self,
        handle: &ZwlrForeignToplevelHandleV1,
        event: ft_handle::Event,
        _data: &(),
        _conn: &Connection,
        _qh: &QueueHandle<Self>,
    ) {
        let Some(index) = state
            .tasks
            .iter()
            .position(|task| task.handle.id() == handle.id())
        else {
            return;
        };

        match event {
            ft_handle::Event::Title { title } => state.tasks[index].title = title,
            ft_handle::Event::State { state: bytes } => {
                let mut activated = false;
                let mut minimized = false;
                for chunk in bytes.chunks_exact(4) {
                    match u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) {
                        1 => minimized = true,
                        2 => activated = true,
                        _ => {}
                    }
                }
                state.tasks[index].activated = activated;
                state.tasks[index].minimized = minimized;
            }
            ft_handle::Event::Done => {
                // `done` zamyka porcję zmian — teraz odświeżamy pasek.
                state.dirty = true;
                state.redraw();
            }
            ft_handle::Event::Closed => {
                state.tasks.retain(|task| task.handle.id() != handle.id());
                state.dirty = true;
                state.redraw();
            }
            _ => {}
        }
    }
}

// --- Start -------------------------------------------------------------------

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .compact()
        .init();

    let connection = Connection::connect_to_env()?;
    let (globals, queue) = registry_queue_init::<Panel>(&connection)?;
    let qh = queue.handle();
    info!("panel PaganDE startuje");

    let compositor = globals.bind::<wl_compositor::WlCompositor, Panel, ()>(&qh, 4..=6, ())?;
    let shm = globals.bind::<wl_shm::WlShm, Panel, ()>(&qh, 1..=1, ())?;
    let layer_shell = globals.bind::<ZwlrLayerShellV1, Panel, ()>(&qh, 1..=4, ())?;
    let manager = globals.bind::<ZwlrForeignToplevelManagerV1, Panel, ()>(&qh, 1..=3, ())?;
    let seat = globals.bind::<wl_seat::WlSeat, Panel, ()>(&qh, 1..=7, ())?;

    let surface = compositor.create_surface(&qh, ());
    let layer_surface = layer_shell.get_layer_surface(
        &surface,
        None, // wyjście wybiera kompozytor
        Layer::Top,
        "pagan-panel".to_string(),
        &qh,
        (),
    );

    // Rezerwujemy pas u góry ekranu — okna nie będą pod niego wchodzić.
    layer_surface.set_anchor(Anchor::Top | Anchor::Left | Anchor::Right);
    layer_surface.set_exclusive_zone(BAR_HEIGHT);
    // Szerokość 0 = „rozciągnij” (bo zakotwiczone z lewej i prawej), wysokość stała.
    layer_surface.set_size(0, BAR_HEIGHT as u32);
    surface.commit();

    let mut panel = Panel {
        connection: connection.clone(),
        qh: qh.clone(),
        compositor,
        layer_shell,
        manager,
        shm,
        layer_surface,
        surface,
        seat,
        pointer_handle: None,
        frame_cb: None,
        size: (0, 0),
        tasks: Vec::new(),
        hover: None,
        hover_launcher: false,
        hover_demo: false,
        pointer_pos: (0.0, 0.0),
        launcher_rect: (0.0, 0.0, 0.0, 0.0),
        demo_rect: (0.0, 0.0, 0.0, 0.0),
        clock: String::new(),
        dirty: false,
        closed: false,
        current: None,
        stale: Vec::new(),
    };
    panel.update_clock();

    let mut event_loop: EventLoop<Panel> = EventLoop::try_new()?;
    let loop_handle: LoopHandle<'_, Panel> = event_loop.handle();
    WaylandSource::new(connection, queue).insert(loop_handle.clone())?;

    // Zegar: odświeżamy co 30 s (i tak zmienia się raz na minutę).
    loop_handle.insert_source(
        calloop::timer::Timer::from_duration(Duration::from_secs(30)),
        |_, _, data: &mut Panel| {
            data.update_clock();
            data.dirty = true;
            data.redraw();
            TimeoutAction::ToDuration(Duration::from_secs(30))
        },
    )?;

    info!("panel gotowy (warstwa górna, {BAR_HEIGHT} px)");
    while !panel.closed {
        if let Err(err) = event_loop.dispatch(Some(Duration::from_millis(250)), &mut panel) {
            error!(%err, "błąd pętli zdarzeń panelu");
            break;
        }
    }
    Ok(())
}