//! 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)>, } 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 { self.entries .iter() .map(|(title, _)| title.clone()) .collect() } /// Pozycje menu o danym indeksie (zawartość rozwinięcia). pub fn items(&self, index: usize) -> Vec { 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 for CompositorState { fn event( _state: &mut Self, _proxy: &PaganAppMenuManagerV1, _event: ::Event, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { } } impl Dispatch for CompositorState { fn event( state: &mut Self, _proxy: &PaganAppMenuV1, event: AppMenuEvent, _data: &(), _conn: &Connection, _qh: &QueueHandle, ) { 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"); } }