🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
//! Klient własnego protokołu `pagan_app_menu_v1` — menu aplikacji w pasku SSD.
//!
//! PO CO: kompozytor PaganDE rysuje pasek tytułu po swojej stronie (wymuszony
//! SSD). Żeby w tym pasku pokazać menu KONKRETNEJ aplikacji, klient publikuje
//! same ETYKIETY górnego poziomu, a kompozytor odsyła zdarzenie `clicked`,
//! gdy użytkownik w którąś kliknie. Zawartość rozwinięcia zna i pokazuje klient.
//!
//! Dzięki temu każde menu może być inne (bo pochodzi od aplikacji), a mimo to
//! wygląda spójnie — rysuje je ten sam pasek PaganDE.
/// Kod wygenerowany z `protocols/pagan-app-menu-v1.xml` (strona klienta).
pub mod protocol {
#[allow(clippy::all)]
pub mod client {
// Wygenerowany kod odwołuje się do crate'ów przez `super::wayland_client`
// oraz do interfejsów rdzenia (np. `wl_surface`) — oba musimy mieć w zasięgu.
#[allow(unused_imports)]
use wayland_client;
#[allow(unused_imports)]
use wayland_client::protocol::*;
pub mod __interfaces {
use wayland_client::protocol::__interfaces::*;
wayland_scanner::generate_interfaces!("../protocols/pagan-app-menu-v1.xml");
}
use self::__interfaces::*;
wayland_scanner::generate_client_code!("../protocols/pagan-app-menu-v1.xml");
}
}
pub use protocol::client::pagan_app_menu_manager_v1::PaganAppMenuManagerV1;
pub use protocol::client::pagan_app_menu_v1::{Event as AppMenuEvent, PaganAppMenuV1};
use wayland_client::{Connection, Dispatch, QueueHandle};
use crate::connection::CompositorState;
/// Definicja menu aplikacji: tytuły górnego poziomu i pozycje każdego z nich.
///
/// PO CO po stronie klienta trzymamy CAŁE menu: kompozytor rysuje tylko tytuły
/// w pasku SSD, a zawartość rozwinięcia pokazuje klient (bo tylko on ją zna).
/// Jedno miejsce definicji = tytuły i pozycje nie mogą się rozjechać.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct AppMenu {
entries: Vec<(String, Vec<String>)>,
}
impl AppMenu {
/// Puste menu.
pub fn new() -> Self {
Self::default()
}
/// Dodaje tytuł górnego poziomu wraz z jego pozycjami (kolejność = kolejność w pasku).
pub fn add(&mut self, title: &str, items: &[&str]) -> &mut Self {
self.entries.push((
title.to_string(),
items.iter().map(|item| item.to_string()).collect(),
));
self
}
/// Tytuły górnego poziomu (to publikujemy kompozytorowi).
pub fn titles(&self) -> Vec<String> {
self.entries
.iter()
.map(|(title, _)| title.clone())
.collect()
}
/// Pozycje menu o danym indeksie (zawartość rozwinięcia).
pub fn items(&self, index: usize) -> Vec<String> {
self.entries
.get(index)
.map(|(_, items)| items.clone())
.unwrap_or_default()
}
/// Czy menu jest puste.
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
/// Zamienia listę etykiet na format protokołu (rozdzielony znakami nowej linii).
///
/// PO CO tak, a nie tablica: napis jest prostszy w oby dwóch bindingach i w pełni
/// wystarcza — etykiety menu nie zawierają znaków nowej linii.
pub fn encode_labels(labels: &[&str]) -> String {
labels.join("\n")
}
#[cfg(test)]
mod tests {
use super::{encode_labels, AppMenu, AppMenuEvent};
#[test]
fn kodowanie_etykiet_jest_odwracalne() {
let labels = ["Plik", "Edycja", "Widok", "Pomoc"];
let encoded = encode_labels(&labels);
assert_eq!(encoded, "Plik\nEdycja\nWidok\nPomoc");
// Po stronie kompozytora ten sam napis jest dzielony z powrotem.
let decoded: Vec<&str> = encoded.split('\n').collect();
assert_eq!(decoded, labels);
}
#[test]
fn pusta_lista_daje_pusty_napis() {
assert!(encode_labels(&[]).is_empty());
}
/// Zdarzenie `clicked` niesie indeks tytułu ORAZ pozycję etykiety.
#[test]
fn zdarzenie_klikniecia_niesie_indeks_i_pozycje() {
let event = AppMenuEvent::Clicked { index: 2, x: 40 };
match event {
AppMenuEvent::Clicked { index, x } => {
assert_eq!(index, 2);
assert_eq!(x, 40);
}
}
}
#[test]
fn menu_trzyma_tytuly_i_pozycje() {
let mut menu = AppMenu::new();
menu.add("Plik", &["Nowy", "Zamknij"])
.add("Pomoc", &["O programie"]);
assert_eq!(menu.titles(), vec!["Plik".to_string(), "Pomoc".to_string()]);
assert_eq!(
menu.items(0),
vec!["Nowy".to_string(), "Zamknij".to_string()]
);
assert_eq!(menu.items(1), vec!["O programie".to_string()]);
assert!(menu.items(9).is_empty());
assert!(!menu.is_empty());
assert!(AppMenu::new().is_empty());
}
}
// Menedżer nie ma zdarzeń — wystarczy puste `Dispatch`, żeby wayland-client
// wiedział, dokąd kierować ewentualne komunikaty.
impl Dispatch<PaganAppMenuManagerV1, ()> for CompositorState {
fn event(
_state: &mut Self,
_proxy: &PaganAppMenuManagerV1,
_event: <PaganAppMenuManagerV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
impl Dispatch<PaganAppMenuV1, ()> for CompositorState {
fn event(
state: &mut Self,
_proxy: &PaganAppMenuV1,
event: AppMenuEvent,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
let AppMenuEvent::Clicked { index, x } = event;
let index = index as usize;
// Klik w tytuł menu: od razu rozwijamy panel pod tą etykietą. Pozycje
// znamy z lokalnej definicji (`AppMenu`), więc nie ma zbędnego round-tripu.
let items = state.app_menu_def.items(index);
state.pending_menu_click = Some((index, x));
if !items.is_empty() {
state.window.open_menu(index, x, items);
state.needs_redraw = true;
}
tracing::debug!(index, x, "kompozytor zgłosił kliknięcie etykiety menu");
}
}