🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
//! Własny protokół `pagan_app_menu_v1` — menu aplikacji w pasku SSD.
//!
//! PO CO osobny protokół: każde menu aplikacji może być INNE (Plik/Edycja/Widok,
//! ale też np. Obraz/Filtr dla edytora grafiki). Kompozytor nie może więc znać
//! zawartości menu z góry. Rozwiązanie dzieli odpowiedzialność:
//!
//! * KLIENT publikuje tylko ETYKIETY górnego poziomu (`set_labels`),
//! * KOMPOZYTOR rysuje je w firmowym pasku tytułu (w jednym rzędzie z przyciskami
//! okna) i odsyła zdarzenie `clicked`, gdy użytkownik w coś kliknie,
//! * KLIENT zna zawartość rozwinięcia i pokazuje je sam (np. jako popup).
//!
//! Dzięki temu wygląd jest spójny (rysuje go kompozytor), a funkcje menu należą
//! do aplikacji.
//!
//! Gdy w pasku nie ma miejsca na etykiety (wąskie okno), zwijają się one do
//! jednego przycisku „burger”. Rozwinięcie burgera pokazuje wtedy WŁAŚNIE menu
//! aplikacji (klik wiersza odsyłamy klientowi). Dopiero gdy klient nie ma menu
//! (obce aplikacje, np. terminale X11), burger rozwija awaryjne menu okna
//! (minimalizuj/maksymalizuj/zamknij).
use std::sync::Mutex;
use wayland_server::{
backend::ClientId, protocol::wl_surface::WlSurface, Dispatch, DisplayHandle, GlobalDispatch,
};
use smithay::utils::{Logical, Point};
use crate::{state::MyCompositor, window::PaganWindow};
/// Kod wygenerowany z `protocols/pagan-app-menu-v1.xml`.
///
/// Ścieżka jest względna do katalogu crate'a (`CARGO_MANIFEST_DIR`), więc XML
/// trzymamy w jednym miejscu w repozytorium i używają go oba projekty.
pub mod protocol {
#[allow(clippy::all)]
pub mod server {
// Wygenerowany kod odwołuje się do crate'ów przez `super::wayland_server`
// oraz do `wayland_backend` bezpośrednio — musimy mieć oba w zasięgu.
#[allow(unused_imports)]
use wayland_server;
// Wygenerowany kod odwołuje się do interfejsów rdzenia (np. `wl_surface`)
// przez `super::wl_surface` — dlatego importujemy je do tego modułu.
#[allow(unused_imports)]
use wayland_server::protocol::*;
pub mod __interfaces {
use wayland_server::protocol::__interfaces::*;
wayland_scanner::generate_interfaces!("../protocols/pagan-app-menu-v1.xml");
}
use self::__interfaces::*;
wayland_scanner::generate_server_code!("../protocols/pagan-app-menu-v1.xml");
}
}
use protocol::server::pagan_app_menu_manager_v1::{
PaganAppMenuManagerV1, Request as ManagerRequest,
};
use protocol::server::pagan_app_menu_v1::{PaganAppMenuV1, Request as MenuRequest};
/// Wersja protokołu, którą reklamujemy klientom.
const VERSION: u32 = 1;
/// Dane protokołu dołączone do obiektu `pagan_app_menu_v1`.
///
/// Trzymamy powierzchnię, żeby przy `set_labels`/`destroyed` znaleźć okno.
#[derive(Debug)]
pub struct AppMenuData {
surface: WlSurface,
}
/// Stan menu trzymany w danych użytkownika OKNA.
///
/// PO CO w danych okna, a nie w `MyCompositor`: etykiety czyta renderowanie
/// (`AsRenderElements`), które nie widzi stanu kompozytora — tak samo jak przy
/// flagach dekoracji.
#[derive(Debug, Default)]
struct WindowMenuState {
labels: Mutex<Vec<String>>,
resource: Mutex<Option<PaganAppMenuV1>>,
}
fn window_state(window: &PaganWindow) -> &WindowMenuState {
window
.user_data()
.get_or_insert_threadsafe(WindowMenuState::default)
}
/// Etykiety menu opublikowane przez klienta (puste = brak menu → burger).
pub fn labels(window: &PaganWindow) -> Vec<String> {
window_state(window)
.labels
.lock()
.map(|labels| labels.clone())
.unwrap_or_default()
}
/// Zapisuje etykiety menu i uchwyt obiektu protokołu dla danego okna.
fn set_labels(window: &PaganWindow, labels: Vec<String>, resource: PaganAppMenuV1) {
let state = window_state(window);
if let Ok(mut stored) = state.labels.lock() {
*stored = labels;
}
if let Ok(mut stored) = state.resource.lock() {
*stored = Some(resource);
}
}
/// Usuwa etykiety okna, gdy klient zniszczył obiekt menu.
fn clear_labels(window: &PaganWindow) {
let state = window_state(window);
if let Ok(mut stored) = state.labels.lock() {
stored.clear();
}
if let Ok(mut stored) = state.resource.lock() {
*stored = None;
}
}
/// Informuje klienta, że użytkownik kliknął etykietę o danym indeksie.
///
/// Wysyłamy także POZIOMĄ pozycję etykiety we współrzędnych powierzchni klienta —
/// dzięki temu klient ustawia rozwinięcie dokładnie pod właściwą etykietą, nie
/// znając układu paska kompozytora.
///
/// PO CO tutaj: obsługa myszy (`input.rs`) nie powinna znać szczegółów protokołu —
/// woła po prostu „okno, kliknięto pozycję N”.
pub fn send_click(window: &PaganWindow, location: Point<i32, Logical>, index: usize) {
let x = window
.ssd_menu_label_rect(location, index)
.map(|rect| rect.loc.x - location.x)
.unwrap_or(0);
let state = window_state(window);
let Ok(guard) = state.resource.lock() else {
return;
};
if let Some(resource) = guard.as_ref() {
resource.clicked(index as u32, x);
}
}
/// Rejestruje global `pagan_app_menu_manager_v1`.
pub fn init(display: &DisplayHandle) {
display.create_global::<MyCompositor, PaganAppMenuManagerV1, ()>(VERSION, ());
tracing::debug!("global pagan_app_menu_manager_v1 zarejestrowany");
}
/// Rozbija napis z protokołu na listę etykiet (rozdzielone znakami nowej linii).
///
/// PO CO osobna funkcja: to granica protokołu — warto ją przetestować bez
/// uruchamiania kompozytora. Puste pozycje odrzucamy, żeby klik nie trafiał
/// w niewidoczne etykiety.
fn parse_labels(raw: &str) -> Vec<String> {
raw.split('\n')
.map(|label| label.trim().to_string())
.filter(|label| !label.is_empty())
.collect()
}
impl GlobalDispatch<PaganAppMenuManagerV1, (), MyCompositor> for MyCompositor {
fn bind(
state: &mut MyCompositor,
_handle: &DisplayHandle,
_client: &wayland_server::Client,
resource: wayland_server::New<PaganAppMenuManagerV1>,
_data: &(),
data_init: &mut wayland_server::DataInit<'_, MyCompositor>,
) {
let manager = data_init.init(resource, ());
state.app_menu_managers.push(manager);
}
}
impl Dispatch<PaganAppMenuManagerV1, (), MyCompositor> for MyCompositor {
fn request(
_state: &mut MyCompositor,
_client: &wayland_server::Client,
_manager: &PaganAppMenuManagerV1,
request: ManagerRequest,
_data: &(),
_handle: &DisplayHandle,
data_init: &mut wayland_server::DataInit<'_, MyCompositor>,
) {
match request {
ManagerRequest::GetAppMenu { id, surface } => {
// Obiekt menu istnieje po to, żeby klient mógł wysłać etykiety
// i odebrać kliknięcia — nic więcej nie musimy tu robić.
let _menu = data_init.init(id, AppMenuData { surface });
}
ManagerRequest::Destroy => {}
}
}
fn destroyed(
state: &mut MyCompositor,
_client: ClientId,
manager: &PaganAppMenuManagerV1,
_data: &(),
) {
state
.app_menu_managers
.retain(|candidate| candidate != manager);
}
}
impl Dispatch<PaganAppMenuV1, AppMenuData, MyCompositor> for MyCompositor {
fn request(
state: &mut MyCompositor,
_client: &wayland_server::Client,
menu: &PaganAppMenuV1,
request: MenuRequest,
data: &AppMenuData,
_handle: &DisplayHandle,
_data_init: &mut wayland_server::DataInit<'_, MyCompositor>,
) {
match request {
MenuRequest::SetLabels { labels: raw } => {
let labels = parse_labels(&raw);
if let Some(window) = state.window_for_surface(&data.surface) {
set_labels(&window, labels, menu.clone());
}
}
MenuRequest::Destroy => {}
}
}
fn destroyed(
state: &mut MyCompositor,
_client: ClientId,
menu: &PaganAppMenuV1,
data: &AppMenuData,
) {
if let Some(window) = state.window_for_surface(&data.surface) {
// Upewniamy się, że usuwamy wpis TEGO obiektu (okno mogło już mieć nowszy).
let is_current = window_state(&window)
.resource
.lock()
.map(|guard| guard.as_ref() == Some(menu))
.unwrap_or(false);
if is_current {
clear_labels(&window);
}
}
}
}
#[cfg(test)]
mod tests {
use super::parse_labels;
#[test]
fn etykiety_sa_rozdzielane_nowa_linia() {
assert_eq!(
parse_labels("Plik\nEdycja\nWidok\nPomoc"),
vec!["Plik", "Edycja", "Widok", "Pomoc"]
);
}
#[test]
fn puste_etykiety_sa_pomijane() {
assert_eq!(parse_labels("\n \nPlik\n\n"), vec!["Plik"]);
assert!(parse_labels("").is_empty());
assert!(parse_labels(" ").is_empty());
}
#[test]
fn etykiety_sa_przycinane() {
assert_eq!(parse_labels(" Plik \n Edycja "), vec!["Plik", "Edycja"]);
}
}