//! 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, // 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, /// Callback klatki — trzymany, by kompozytor nie wysyłał zdarzeń do „martwego” obiektu. frame_cb: Option, /// Rozmiar powierzchni z ostatniego `configure` (px logiczne). size: (i32, i32), tasks: Vec, /// Indeks kafelka pod kursorem. hover: Option, /// 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, stale: Vec, } 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 { 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 = Vec::new(); for name in ["pagan-demo", "pagan-toolkit-demo"] { if which(name) { candidates.push(name.into()); } } // Układ deweloperski: /pagan-panel/target/debug/pagan-panel, // więc demo leży w /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 { 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 { 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 for Panel { fn event( _state: &mut Self, _proxy: &wl_registry::WlRegistry, _event: wl_registry::Event, _data: &GlobalListContents, _conn: &Connection, _qh: &QueueHandle, ) { } } /// 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, ) { } } )* }; } ignore_events!( wl_compositor::WlCompositor, wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_buffer::WlBuffer, wl_output::WlOutput, ); impl Dispatch for Panel { fn event( state: &mut Self, _proxy: &wl_callback::WlCallback, event: wl_callback::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 for Panel { fn event( _state: &mut Self, _proxy: &wl_surface::WlSurface, _event: wl_surface::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { } } impl Dispatch for Panel { fn event( state: &mut Self, seat: &wl_seat::WlSeat, event: wl_seat::Event, _data: &(), _conn: &Connection, qh: &QueueHandle, ) { 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 for Panel { fn event( state: &mut Self, _pointer: &wl_pointer::WlPointer, event: wl_pointer::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 for Panel { fn event( _state: &mut Self, _proxy: &ZwlrLayerShellV1, _event: layer_shell::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { } } impl Dispatch for Panel { fn event( state: &mut Self, layer_surface: &ZwlrLayerSurfaceV1, event: layer_surface::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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 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, ) { 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 for Panel { fn event( state: &mut Self, handle: &ZwlrForeignToplevelHandleV1, event: ft_handle::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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> { 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::(&connection)?; let qh = queue.handle(); info!("panel PaganDE startuje"); let compositor = globals.bind::(&qh, 4..=6, ())?; let shm = globals.bind::(&qh, 1..=1, ())?; let layer_shell = globals.bind::(&qh, 1..=4, ())?; let manager = globals.bind::(&qh, 1..=3, ())?; let seat = globals.bind::(&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 = 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(()) }