🔒 Repository is read-only – file editing is disabled.
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(())
}