← PaganDE

Commit d2db012

16
plików
+644
dodanych
-95
usuniętych
@@ -3,20 +3,25 @@
3 3 Środowisko graficzne Wayland budowane od zera w Rust:
4 4
5 5 - **`pagan-compositor`** — kompozytor (serwer) na [Smithay 0.7](https://github.com/Smithay/smithay):
6 - floating window manager, renderer OpenGL (`glow`), **hybrydowe CSD**, cień pod oknami.
6 + floating window manager, renderer OpenGL (`glow`), **wymuszone dekoracje serwerowe
7 + (SSD)** z menu aplikacji w pasku, cień pod oknami, doklejanie okien (Aero Snap).
7 8 - **`pagan-toolkit`** — własna biblioteka UI dla klientów: `wayland-client` + `memmap2`
8 9 (SHM) + `cairo-rs`. Bez Qt/GTK/SCTK/iced. Antyaliasing, zaokrąglone rogi, wspólny
9 10 pasek tytułu, lekka lokalizacja PL/EN.
10 11 - **`smithay-0.7.0`** — vendored źródła Smithay (build offline, powtarzalny).
11 12
12 -## Filozofia: hybrydowe CSD
13 +## Filozofia: spójne dekoracje serwerowe (SSD)
13 14
14 -Klient rysuje **całe** okno (zaokrąglone tło, pasek tytułu, menu, przyciski).
15 -Kompozytor dokłada tylko **cień** (i opcjonalnie maskę rogów shaderem). Dlatego:
15 +Kompozytor rysuje pasek tytułu dla **każdego** okna i wymusza tryb serwerowy
16 +(`xdg-decoration` → `ServerSide`). Obce aplikacje (GTK, X11) nie rysują wtedy
17 +swoich dekoracji, więc cała sesja wygląda jednolicie.
16 18
17 -- menu i przyciski są zawsze idealnie wyrównane — rysuje je ten sam kod Cairo,
18 -- nie trzeba pisać niestandardowych protokołów Waylanda,
19 -- move/resize działają standardowo: klient wysyła `xdg_toplevel.move` / `resize`.
19 +Menu aplikacji jest jedynym elementem, którego kompozytor nie zna z góry —
20 +aplikacje mają różne menu. Rozwiązuje to **własny protokół** `pagan_app_menu_v1`:
21 +klient publikuje etykiety górnego poziomu, kompozytor rysuje je w pasku i odsyła
22 +kliknięcia. Szczegóły: [Dekoracje serwerowe (SSD)](#dekoracje-serwerowe-ssd-i-menu-aplikacji-w-pasku).
23 +
24 +Aplikacje bez tego protokołu dostają w pasku przycisk „burger”.
20 25
21 26 ## Wymagania
22 27
@@ -115,6 +120,17 @@ shadow_offset_y = 6
115 120 shadow_radius = 12 # promień rogów sylwetki cienia
116 121 cursor_fill = #f2f2f7 # awaryjny kursor (gdy brak motywu XCursor)
117 122 cursor_border = #14141a
123 +
124 +# Dekoracje serwerowe (SSD) — płaski, firmowy pasek tytułu
125 +ssd_bar = #1c202b # pasek okna AKTYWNEGO
126 +ssd_unfocused = #14161c # pasek okna NIEAKTYWNEGO
127 +ssd_border = #0d0e13 # 1 px linia pod paskiem
128 +ssd_text = #d6dae4 # tytuł + etykiety menu
129 +ssd_text_unfocused = #868b96
130 +ssd_icon_close = #e06c6c # glif „zamknij”
131 +ssd_radius = 12
132 +ssd_font_px = 13
133 +ssd_menu_min_width = 560 # węższe okno zwija menu do burgera
118 134 ```
119 135
120 136 **Zmiany działają od razu** — kompozytor obserwuje plik przez `inotify` i wczytuje
@@ -122,6 +138,43 @@ go od nowa po każdym zapisie (bez restartu). `Ctrl+Alt+R` wymusza przeładowani
122 138 ręcznie. Kolory to `#RRGGBB` / `#RRGGBBAA`. Implementacja: `src/theme.rs`
123 139 (7 testów jednostkowych).
124 140
141 +### Dekoracje serwerowe (SSD) i menu aplikacji w pasku
142 +
143 +Kompozytor **wymusza** tryb dekoracji serwerowych (`xdg-decoration` → `ServerSide`)
144 +dla każdego okna najwyższego poziomu, żeby całe środowisko wyglądało spójnie —
145 +niezależnie od tego, czy aplikacja jest Waylandowa, GTK, czy X11. Pasek jest
146 +płaski (bez gradientów), z zaokrąglonymi górnymi rogami i cieniem pod ramką.
147 +
148 +W pasku, w **jednym rzędzie**, leżą: menu aplikacji (po lewej), tytuł okna
149 +(wyśrodkowany, o ile się mieści) oraz przyciski minimalizuj/maksymalizuj/zamknij
150 +(po prawej). Okno nieaktywne jest przygaszone i pozbawione tytułu z akcentem —
151 +dzięki temu od razu widać, gdzie trafi klawiatura.
152 +
153 +#### Własny protokół: `pagan_app_menu_v1`
154 +
155 +Każda aplikacja ma **inne** menu, więc kompozytor nie może znać jego zawartości.
156 +Rozdzielamy odpowiedzialność:
157 +
158 +* **klient** publikuje same ETYKIETY górnego poziomu (`set_labels`),
159 +* **kompozytor** rysuje je w firmowym pasku i odsyła zdarzenie `clicked(index)`,
160 + gdy użytkownik w coś kliknie,
161 +* **klient** zna zawartość rozwinięcia i pokazuje je sam (docelowo jako `xdg_popup`).
162 +
163 +Opis protokołu: [`protocols/pagan-app-menu-v1.xml`](protocols/pagan-app-menu-v1.xml).
164 +Kod generuje `wayland-scanner` po obu stronach (te same etykiety na serwerze
165 +w `pagan-compositor/src/app_menu.rs` i u klienta w `pagan-toolkit/src/app_menu.rs`).
166 +Użycie w aplikacji:
167 +
168 +```rust
169 +pagan_toolkit::publish_app_menu(&mut state, &["Plik", "Edycja", "Widok", "Pomoc"])?;
170 +```
171 +
172 +Aplikacje, które tego protokołu nie używają (np. terminale X11), dostają w pasku
173 +pojedynczy przycisk „burger”, rozwijający menu okna.
174 +
175 +Tekst na pasku (tytuł i etykiety menu) rysuje kompozytor: rasteryzuje go Cairo
176 +i wysyła na GPU jako teksturę (`pagan-compositor/src/text.rs`, cache per napis).
177 +
125 178 ### Testy interakcji bez machania myszką (dev)
126 179
127 180 Na backendzie `winit` kompozytor jest zwykłym oknem X11, więc zdarzenia myszy
@@ -138,10 +191,12 @@ DISPLAY=:1 python3 tools/x11_probe.py find 1280 800 # origin okna klienta
138 191
139 192 | Zadanie | Status | Gdzie |
140 193 |---|---|---|
141 -| 1. Szkielet kompozytora (Smithay, winit, floating, move/resize, Z-order, xdg-decoration=client) | ✅ winit + ✅ udev | `pagan-compositor/src/` |
194 +| 1. Szkielet kompozytora (Smithay, winit, floating, move/resize, Z-order, xdg-decoration=server) | ✅ winit + ✅ udev | `pagan-compositor/src/` |
142 195 | 1b. Doklejanie do krawędzi/rogów (Aero Snap) + „wyskakiwanie” ze snapa | ✅ (`snap.rs`, 10 testów) | `pagan-compositor/src/snap.rs`, `grabs.rs` |
143 196 | 2. Szkielet toolkitu (Wayland, SHM/memmap2, Cairo, CSD, hit-testing) | ✅ | `pagan-toolkit/src/` |
144 197 | 3. Shadery GLSL (zaokrąglenia + cień) | ✅ **cień na GPU** (pixel shader) | `pagan-compositor/src/shaders/`, `window.rs` |
198 +| 4. Wymuszone dekoracje serwerowe (SSD) + menu aplikacji w pasku | ✅ | `pagan-compositor/src/window.rs`, `text.rs`, `app_menu.rs` |
199 +| 5. Własny protokół `pagan_app_menu_v1` (menu per aplikacja) | ✅ | `protocols/`, `pagan-compositor/src/app_menu.rs`, `pagan-toolkit/src/app_menu.rs` |
145 200
146 201 ## Ograniczenia i następne kroki
147 202
@@ -1279,7 +1279,10 @@ dependencies = [
1279 1279 "smithay-drm-extras",
1280 1280 "tracing",
1281 1281 "tracing-subscriber",
1282 + "wayland-backend",
1282 1283 "wayland-protocols-wlr",
1284 + "wayland-scanner",
1285 + "wayland-server",
1283 1286 "xcursor",
1284 1287 ]
1285 1288
@@ -46,6 +46,12 @@ inotify = { version = "0.11.5", default-features = false }
46 46 # Cairo jest już pakietem w PaganOS (używa go też toolkit), więc nie dokładamy
47 47 # nowej zależności systemowej — tylko tę samą, sprawdzoną bibliotekę.
48 48 cairo-rs = "0.22"
49 +# Własny protokół menu aplikacji (pagan-app-menu-v1): generowanie kodu serwera
50 +# z XML-owego opisu. `wayland-server` jest potrzebny, bo wygenerowany kod
51 +# odwołuje się do niego bezpośrednio po nazwie crate'a.
52 +wayland-server = "0.31"
53 +wayland-backend = "0.3"
54 +wayland-scanner = "0.31"
49 55
50 56 [features]
51 57 default = []
52 58 new file mode 100644
@@ -0,0 +1,258 @@
1 +//! Własny protokół `pagan_app_menu_v1` — menu aplikacji w pasku SSD.
2 +//!
3 +//! PO CO osobny protokół: każde menu aplikacji może być INNE (Plik/Edycja/Widok,
4 +//! ale też np. Obraz/Filtr dla edytora grafiki). Kompozytor nie może więc znać
5 +//! zawartości menu z góry. Rozwiązanie dzieli odpowiedzialność:
6 +//!
7 +//! * KLIENT publikuje tylko ETYKIETY górnego poziomu (`set_labels`),
8 +//! * KOMPOZYTOR rysuje je w firmowym pasku tytułu (w jednym rzędzie z przyciskami
9 +//! okna) i odsyła zdarzenie `clicked`, gdy użytkownik w coś kliknie,
10 +//! * KLIENT zna zawartość rozwinięcia i pokazuje je sam (np. jako popup).
11 +//!
12 +//! Dzięki temu wygląd jest spójny (rysuje go kompozytor), a funkcje menu należą
13 +//! do aplikacji.
14 +//!
15 +//! Aplikacje, które tego protokołu nie używają (obce), dostają w pasku pojedynczy
16 +//! przycisk „burger” zamiast etykiet.
17 +
18 +use std::sync::Mutex;
19 +
20 +use wayland_server::{
21 + backend::ClientId, protocol::wl_surface::WlSurface, Dispatch, DisplayHandle, GlobalDispatch,
22 +};
23 +
24 +use crate::{state::MyCompositor, window::PaganWindow};
25 +
26 +/// Kod wygenerowany z `protocols/pagan-app-menu-v1.xml`.
27 +///
28 +/// Ścieżka jest względna do katalogu crate'a (`CARGO_MANIFEST_DIR`), więc XML
29 +/// trzymamy w jednym miejscu w repozytorium i używają go oba projekty.
30 +pub mod protocol {
31 + #[allow(clippy::all)]
32 + pub mod server {
33 + // Wygenerowany kod odwołuje się do crate'ów przez `super::wayland_server`
34 + // oraz do `wayland_backend` bezpośrednio — musimy mieć oba w zasięgu.
35 + #[allow(unused_imports)]
36 + use wayland_server;
37 + // Wygenerowany kod odwołuje się do interfejsów rdzenia (np. `wl_surface`)
38 + // przez `super::wl_surface` — dlatego importujemy je do tego modułu.
39 + #[allow(unused_imports)]
40 + use wayland_server::protocol::*;
41 +
42 + pub mod __interfaces {
43 + use wayland_server::protocol::__interfaces::*;
44 + wayland_scanner::generate_interfaces!("../protocols/pagan-app-menu-v1.xml");
45 + }
46 + use self::__interfaces::*;
47 +
48 + wayland_scanner::generate_server_code!("../protocols/pagan-app-menu-v1.xml");
49 + }
50 +}
51 +
52 +use protocol::server::pagan_app_menu_manager_v1::{
53 + PaganAppMenuManagerV1, Request as ManagerRequest,
54 +};
55 +use protocol::server::pagan_app_menu_v1::{PaganAppMenuV1, Request as MenuRequest};
56 +
57 +/// Wersja protokołu, którą reklamujemy klientom.
58 +const VERSION: u32 = 1;
59 +
60 +/// Dane protokołu dołączone do obiektu `pagan_app_menu_v1`.
61 +///
62 +/// Trzymamy powierzchnię, żeby przy `set_labels`/`destroyed` znaleźć okno.
63 +#[derive(Debug)]
64 +pub struct AppMenuData {
65 + surface: WlSurface,
66 +}
67 +
68 +/// Stan menu trzymany w danych użytkownika OKNA.
69 +///
70 +/// PO CO w danych okna, a nie w `MyCompositor`: etykiety czyta renderowanie
71 +/// (`AsRenderElements`), które nie widzi stanu kompozytora — tak samo jak przy
72 +/// flagach dekoracji.
73 +#[derive(Debug, Default)]
74 +struct WindowMenuState {
75 + labels: Mutex<Vec<String>>,
76 + resource: Mutex<Option<PaganAppMenuV1>>,
77 +}
78 +
79 +fn window_state(window: &PaganWindow) -> &WindowMenuState {
80 + window
81 + .user_data()
82 + .get_or_insert_threadsafe(WindowMenuState::default)
83 +}
84 +
85 +/// Etykiety menu opublikowane przez klienta (puste = brak menu → burger).
86 +pub fn labels(window: &PaganWindow) -> Vec<String> {
87 + window_state(window)
88 + .labels
89 + .lock()
90 + .map(|labels| labels.clone())
91 + .unwrap_or_default()
92 +}
93 +
94 +/// Zapisuje etykiety menu i uchwyt obiektu protokołu dla danego okna.
95 +fn set_labels(window: &PaganWindow, labels: Vec<String>, resource: PaganAppMenuV1) {
96 + let state = window_state(window);
97 + if let Ok(mut stored) = state.labels.lock() {
98 + *stored = labels;
99 + }
100 + if let Ok(mut stored) = state.resource.lock() {
101 + *stored = Some(resource);
102 + }
103 +}
104 +
105 +/// Usuwa etykiety okna, gdy klient zniszczył obiekt menu.
106 +fn clear_labels(window: &PaganWindow) {
107 + let state = window_state(window);
108 + if let Ok(mut stored) = state.labels.lock() {
109 + stored.clear();
110 + }
111 + if let Ok(mut stored) = state.resource.lock() {
112 + *stored = None;
113 + }
114 +}
115 +
116 +/// Informuje klienta, że użytkownik kliknął etykietę o danym indeksie.
117 +///
118 +/// PO CO tutaj: obsługa myszy (`input.rs`) nie powinna znać szczegółów protokołu —
119 +/// woła po prostu „okno, kliknięto pozycję N”.
120 +pub fn send_click(window: &PaganWindow, index: usize) {
121 + let state = window_state(window);
122 + let Ok(guard) = state.resource.lock() else {
123 + return;
124 + };
125 + if let Some(resource) = guard.as_ref() {
126 + resource.clicked(index as u32);
127 + }
128 +}
129 +
130 +/// Rejestruje global `pagan_app_menu_manager_v1`.
131 +pub fn init(display: &DisplayHandle) {
132 + display.create_global::<MyCompositor, PaganAppMenuManagerV1, ()>(VERSION, ());
133 + tracing::debug!("global pagan_app_menu_manager_v1 zarejestrowany");
134 +}
135 +
136 +/// Rozbija napis z protokołu na listę etykiet (rozdzielone znakami nowej linii).
137 +///
138 +/// PO CO osobna funkcja: to granica protokołu — warto ją przetestować bez
139 +/// uruchamiania kompozytora. Puste pozycje odrzucamy, żeby klik nie trafiał
140 +/// w niewidoczne etykiety.
141 +fn parse_labels(raw: &str) -> Vec<String> {
142 + raw.split('\n')
143 + .map(|label| label.trim().to_string())
144 + .filter(|label| !label.is_empty())
145 + .collect()
146 +}
147 +
148 +impl GlobalDispatch<PaganAppMenuManagerV1, (), MyCompositor> for MyCompositor {
149 + fn bind(
150 + state: &mut MyCompositor,
151 + _handle: &DisplayHandle,
152 + _client: &wayland_server::Client,
153 + resource: wayland_server::New<PaganAppMenuManagerV1>,
154 + _data: &(),
155 + data_init: &mut wayland_server::DataInit<'_, MyCompositor>,
156 + ) {
157 + let manager = data_init.init(resource, ());
158 + state.app_menu_managers.push(manager);
159 + }
160 +}
161 +
162 +impl Dispatch<PaganAppMenuManagerV1, (), MyCompositor> for MyCompositor {
163 + fn request(
164 + _state: &mut MyCompositor,
165 + _client: &wayland_server::Client,
166 + _manager: &PaganAppMenuManagerV1,
167 + request: ManagerRequest,
168 + _data: &(),
169 + _handle: &DisplayHandle,
170 + data_init: &mut wayland_server::DataInit<'_, MyCompositor>,
171 + ) {
172 + match request {
173 + ManagerRequest::GetAppMenu { id, surface } => {
174 + // Obiekt menu istnieje po to, żeby klient mógł wysłać etykiety
175 + // i odebrać kliknięcia — nic więcej nie musimy tu robić.
176 + let _menu = data_init.init(id, AppMenuData { surface });
177 + }
178 + ManagerRequest::Destroy => {}
179 + }
180 + }
181 +
182 + fn destroyed(
183 + state: &mut MyCompositor,
184 + _client: ClientId,
185 + manager: &PaganAppMenuManagerV1,
186 + _data: &(),
187 + ) {
188 + state
189 + .app_menu_managers
190 + .retain(|candidate| candidate != manager);
191 + }
192 +}
193 +
194 +impl Dispatch<PaganAppMenuV1, AppMenuData, MyCompositor> for MyCompositor {
195 + fn request(
196 + state: &mut MyCompositor,
197 + _client: &wayland_server::Client,
198 + menu: &PaganAppMenuV1,
199 + request: MenuRequest,
200 + data: &AppMenuData,
201 + _handle: &DisplayHandle,
202 + _data_init: &mut wayland_server::DataInit<'_, MyCompositor>,
203 + ) {
204 + match request {
205 + MenuRequest::SetLabels { labels: raw } => {
206 + let labels = parse_labels(&raw);
207 + if let Some(window) = state.window_for_surface(&data.surface) {
208 + set_labels(&window, labels, menu.clone());
209 + }
210 + }
211 + MenuRequest::Destroy => {}
212 + }
213 + }
214 +
215 + fn destroyed(
216 + state: &mut MyCompositor,
217 + _client: ClientId,
218 + menu: &PaganAppMenuV1,
219 + data: &AppMenuData,
220 + ) {
221 + if let Some(window) = state.window_for_surface(&data.surface) {
222 + // Upewniamy się, że usuwamy wpis TEGO obiektu (okno mogło już mieć nowszy).
223 + let is_current = window_state(&window)
224 + .resource
225 + .lock()
226 + .map(|guard| guard.as_ref() == Some(menu))
227 + .unwrap_or(false);
228 + if is_current {
229 + clear_labels(&window);
230 + }
231 + }
232 + }
233 +}
234 +
235 +#[cfg(test)]
236 +mod tests {
237 + use super::parse_labels;
238 +
239 + #[test]
240 + fn etykiety_sa_rozdzielane_nowa_linia() {
241 + assert_eq!(
242 + parse_labels("Plik\nEdycja\nWidok\nPomoc"),
243 + vec!["Plik", "Edycja", "Widok", "Pomoc"]
244 + );
245 + }
246 +
247 + #[test]
248 + fn puste_etykiety_sa_pomijane() {
249 + assert_eq!(parse_labels("\n \nPlik\n\n"), vec!["Plik"]);
250 + assert!(parse_labels("").is_empty());
251 + assert!(parse_labels(" ").is_empty());
252 + }
253 +
254 + #[test]
255 + fn etykiety_sa_przycinane() {
256 + assert_eq!(parse_labels(" Plik \n Edycja "), vec!["Plik", "Edycja"]);
257 + }
258 +}
@@ -409,11 +409,11 @@ impl MyCompositor {
409 409 }
410 410 None => {
411 411 self.close_all_ssd_menus();
412 - // Etykieta menu aplikacji (Plik/Edycja/Widok/Pomoc) leży w pasku,
413 - // ale nie jest przyciskiem okna. Na razie klik tylko konsumujemy
414 - // (nie przeciągamy okna za menu). Docelowo trafi do aplikacji
415 - // przez własny protokół menu.
416 - if window.ssd_menu_label_at(location, point).is_some() {
412 + // Etykieta menu aplikacji (np. Plik/Edycja/Widok/Pomoc) leży w pasku,
413 + // ale nie jest przyciskiem okna. Informujemy o kliknięciu KLIENTA —
414 + // on zna zawartość swojego menu i pokaże rozwinięcie.
415 + if let Some(index) = window.ssd_menu_label_at(location, point) {
416 + crate::app_menu::send_click(&window, index);
417 417 return true;
418 418 }
419 419 // Przeciąganie za pasek — ten sam grab co przy `xdg_toplevel.move`.
@@ -13,6 +13,7 @@
13 13 //! - `focus` – cele fokusa (mysz/klawiatura),
14 14 //! - `shaders` – wbudowane shadery GLSL (zaokrąglenia + cień).
15 15
16 +mod app_menu;
16 17 mod cursor;
17 18 mod focus;
18 19 mod foreign_toplevel;
@@ -378,28 +378,6 @@ pub fn top_rounded_rect_element(
378 378 )
379 379 }
380 380
381 -/// Buduje element paska z pionowym gradientem i zaokrąglonymi górnymi rogami.
382 -///
383 -/// PO CO: firmowy pasek tytułu PaganDE — gradient to element tożsamości wizualnej.
384 -pub fn gradient_bar_element(
385 - renderer: &mut GlesRenderer,
386 - area: Rectangle<i32, Logical>,
387 - top: Color32F,
388 - bottom: Color32F,
389 - radius: f32,
390 -) -> Option<PixelShaderElement> {
391 - shape_element(
392 - renderer,
393 - area,
394 - area.size,
395 - 0.0,
396 - (radius, radius, 0.0, 0.0),
397 - top,
398 - bottom,
399 - true,
400 - )
401 -}
402 -
403 381 /// Buduje element obróconego prostokąta o zaokrąglonych rogach.
404 382 ///
405 383 /// PO CO osobno: ikona „X" (zamknij) to dwa prostokąty obrócone o ±45°.
@@ -209,6 +209,13 @@ pub struct MyCompositor {
209 209 /// Stan protokołu `wlr-foreign-toplevel-management` (lista okien dla paneli).
210 210 pub foreign_toplevel: ForeignToplevelState,
211 211
212 + /// Uchwyty globala `pagan_app_menu_manager_v1` (menu aplikacji w pasku SSD).
213 + ///
214 + /// PO CO trzymamy: obiekty menedżera żyją tak długo, jak ich klient — musimy
215 + /// je znać, żeby posprzątać po rozłączeniu i móc wysyłać zdarzenia.
216 + pub app_menu_managers:
217 + Vec<crate::app_menu::protocol::server::pagan_app_menu_manager_v1::PaganAppMenuManagerV1>,
218 +
212 219 /// Podgląd („duch”) geometrii docelowej przy doklejaniu okna.
213 220 ///
214 221 /// PO CO: pokazujemy, gdzie okno wyląduje, ZANIM użytkownik puści przycisk —
@@ -329,6 +336,9 @@ impl MyCompositor {
329 336 // Lista i sterowanie oknami dla paneli/pasków zadań.
330 337 crate::foreign_toplevel::init(&dh);
331 338
339 + // Menu aplikacji w pasku SSD (własny protokół PaganDE).
340 + crate::app_menu::init(&dh);
341 +
332 342 // XWayland: `xwayland_shell` (skojarzenie okna X11 z wl_surface) oraz grab
333 343 // klawiatury X11. `XWaylandKeyboardGrabState` nie wymaga pola — trzyma go
334 344 // display; tworzymy wyłącznie po to, by zarejestrować global.
@@ -377,6 +387,7 @@ impl MyCompositor {
377 387 dnd_icon: None,
378 388 minimized: Vec::new(),
379 389 foreign_toplevel: ForeignToplevelState::new(),
390 + app_menu_managers: Vec::new(),
380 391 snap_preview: None,
381 392 dmabuf_state: DmabufState::new(),
382 393 dmabuf_global: None,
@@ -270,24 +270,6 @@ impl PaganWindow {
270 270 self.ssd_state().focused.store(focused, Ordering::Relaxed);
271 271 }
272 272
273 - /// Identyfikator aplikacji (Wayland: `app_id`; X11: klasa okna).
274 - ///
275 - /// PO CO przez `with_states`: `app_id` i `title` żyją w danych roli powierzchni
276 - /// xdg (`XdgToplevelSurfaceData`), a nie w stanie konfiguracji toplevela.
277 - fn app_id(&self) -> Option<String> {
278 - if let Some(toplevel) = self.toplevel() {
279 - let surface = toplevel.wl_surface().clone();
280 - return with_states(&surface, |states| {
281 - states
282 - .data_map
283 - .get::<XdgToplevelSurfaceData>()
284 - .and_then(|data| data.lock().ok())
285 - .and_then(|data| data.app_id.clone())
286 - });
287 - }
288 - self.x11_surface().map(|x11| x11.class())
289 - }
290 -
291 273 /// Tytuł okna (Wayland: `xdg_toplevel`; X11: `_NET_WM_NAME`).
292 274 pub fn window_title(&self) -> Option<String> {
293 275 if let Some(toplevel) = self.toplevel() {
@@ -306,23 +288,14 @@ impl PaganWindow {
306 288 .filter(|value| !value.is_empty())
307 289 }
308 290
309 - /// Czy okno należy do aplikacji PaganDE (czyli ma nasze menu aplikacji)?
310 - ///
311 - /// PO CO: etykiety menu aplikacji pokazujemy tylko tym oknom, które faktycznie
312 - /// je mają. Aplikacje obce dostają burger (rozwijający menu okna). Docelowo
313 - /// zamiast konwencji `app_id` będzie tu mały własny protokół menu.
314 - pub fn has_pagan_menu(&self) -> bool {
315 - self.app_id()
316 - .map(|id| id.starts_with("org.pagan."))
317 - .unwrap_or(false)
318 - }
319 -
320 291 /// Czy pasek ma pokazać pełne etykiety menu (zamiast burgera)?
321 292 ///
322 - /// Warunki: okno ma menu aplikacji ORAZ jest wystarczająco szerokie.
323 - /// Wąskie okno zwija menu do burgera, żeby nie zabrakło miejsca na tytuł.
293 + /// Warunki: klient opublikował menu (własny protokół `pagan_app_menu_v1`)
294 + /// ORAZ okno jest wystarczająco szerokie. Wąskie okno zwija menu do burgera,
295 + /// żeby nie zabrakło miejsca na tytuł.
324 296 pub fn ssd_bar_uses_labels(&self) -> bool {
325 - self.has_pagan_menu() && self.geometry().size.w >= theme::current().ssd_menu_min_width
297 + !crate::app_menu::labels(self).is_empty()
298 + && self.geometry().size.w >= theme::current().ssd_menu_min_width
326 299 }
327 300
328 301 /// Indeks etykiety menu pod punktem (jeśli któraś trafiona).
@@ -335,8 +308,9 @@ impl PaganWindow {
335 308 return None;
336 309 }
337 310 let bar = self.titlebar_rect(location);
338 - let labels = app_menu_labels();
339 - let widths = menu_label_widths(labels, theme::current().ssd_font_px as f64);
311 + let labels = crate::app_menu::labels(self);
312 + let refs: Vec<&str> = labels.iter().map(String::as_str).collect();
313 + let widths = menu_label_widths(&refs, theme::current().ssd_font_px as f64);
340 314 menu_label_rects(bar, &widths)
341 315 .iter()
342 316 .position(|rect| rect.to_f64().contains(point))
@@ -711,12 +685,14 @@ impl AsRenderElements<GlesRenderer> for PaganWindow {
711 685 if self.wants_ssd() {
712 686 // Lista elementów jest OD PRZODU DO TYŁU, więc tekst i glify muszą być
713 687 // wypchnięte PRZED belką — inaczej belka by je zasłoniła.
714 - let labels_mode = self.ssd_bar_uses_labels();
688 + let menu_labels = crate::app_menu::labels(self);
689 + let labels_mode =
690 + !menu_labels.is_empty() && self.geometry().size.w >= theme.ssd_menu_min_width;
715 691
716 692 if labels_mode {
717 - let labels = app_menu_labels();
718 - let widths = menu_label_widths(labels, font_px);
719 - for (rect, label) in menu_label_rects(bar, &widths).into_iter().zip(labels) {
693 + let refs: Vec<&str> = menu_labels.iter().map(String::as_str).collect();
694 + let widths = menu_label_widths(&refs, font_px);
695 + for (rect, label) in menu_label_rects(bar, &widths).into_iter().zip(refs) {
720 696 push_text(
721 697 &mut elements,
722 698 renderer,
@@ -752,8 +728,8 @@ impl AsRenderElements<GlesRenderer> for PaganWindow {
752 728 // Tytuł okna — tylko gdy zmieści się między menu a przyciskami.
753 729 if let Some(title) = self.window_title() {
754 730 let menu_right = if labels_mode {
755 - let labels = app_menu_labels();
756 - let widths = menu_label_widths(labels, font_px);
731 + let refs: Vec<&str> = menu_labels.iter().map(String::as_str).collect();
732 + let widths = menu_label_widths(&refs, font_px);
757 733 menu_label_rects(bar, &widths)
758 734 .last()
759 735 .map(|rect| rect.loc.x + rect.size.w)
@@ -821,24 +797,6 @@ impl AsRenderElements<GlesRenderer> for PaganWindow {
821 797 }
822 798 }
823 799
824 -/// Etykiety menu aplikacji, lokalizowane przez `PAGAN_LANG`.
825 -///
826 -/// PO CO cache (`OnceLock`): odczyt zmiennej środowiskowej i wybór tablicy
827 -/// wykonujemy raz na proces — funkcja jest wołana przy każdej klatce i kliku.
828 -fn app_menu_labels() -> &'static [&'static str] {
829 - static LABELS: OnceLock<&'static [&'static str]> = OnceLock::new();
830 - LABELS.get_or_init(|| {
831 - let polish = std::env::var("PAGAN_LANG")
832 - .map(|value| value.trim().to_ascii_lowercase().starts_with("pl"))
833 - .unwrap_or(false);
834 - if polish {
835 - &["Plik", "Edycja", "Widok", "Pomoc"]
836 - } else {
837 - &["File", "Edit", "View", "Help"]
838 - }
839 - })
840 -}
841 -
842 800 /// Szerokości klikalnych pól etykiet menu (sam tekst + odstęp po bokach).
843 801 fn menu_label_widths(labels: &[&str], font_px: f64) -> Vec<i32> {
844 802 labels
@@ -253,9 +253,11 @@ dependencies = [
253 253 "tempfile",
254 254 "tracing",
255 255 "tracing-subscriber",
256 + "wayland-backend",
256 257 "wayland-client",
257 258 "wayland-protocols",
258 259 "wayland-protocols-wlr",
260 + "wayland-scanner",
259 261 ]
260 262
261 263 [[package]]
@@ -18,6 +18,9 @@ wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
18 18 wayland-protocols-wlr = { version = "0.3", features = ["client"] }
19 19 memmap2 = "0.9"
20 20 cairo-rs = "0.22"
21 +# Własny protokół menu aplikacji (pagan-app-menu-v1) — generowanie kodu klienta.
22 +wayland-scanner = "0.31"
23 +wayland-backend = "0.3"
21 24 tempfile = "3"
22 25 tracing = "0.1"
23 26 tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ -17,7 +17,7 @@
17 17
18 18 use tracing_subscriber::EnvFilter;
19 19
20 -use pagan_toolkit::connection::{create_window, render_and_commit};
20 +use pagan_toolkit::connection::{create_window, publish_app_menu, render_and_commit};
21 21 use pagan_toolkit::i18n::{tr, Locale, MsgKey};
22 22 use pagan_toolkit::window::WindowAction;
23 23
@@ -25,6 +25,19 @@ use pagan_toolkit::window::WindowAction;
25 25 const WINDOW_WIDTH: i32 = 800;
26 26 const WINDOW_HEIGHT: i32 = 500;
27 27
28 +/// Pozycje menu aplikacji w kolejności wyświetlania w pasku tytułu.
29 +///
30 +/// PO CO osobna funkcja: te same etykiety publikujemy kompozytorowi (rysuje je
31 +/// w pasku SSD) i używamy do rozpoznania kliknięcia w `pending_menu_click`.
32 +fn menu_labels(locale: Locale) -> [&'static str; 4] {
33 + [
34 + tr(locale, MsgKey::MenuFile),
35 + tr(locale, MsgKey::MenuEdit),
36 + tr(locale, MsgKey::MenuView),
37 + tr(locale, MsgKey::MenuHelp),
38 + ]
39 +}
40 +
28 41 fn main() {
29 42 init_logging();
30 43
@@ -43,6 +56,12 @@ fn main() {
43 56 };
44 57 let qh = event_queue.handle();
45 58
59 + // 1b. Publikujemy menu aplikacji kompozytorowi — narysuje je w pasku tytułu
60 + // (we własnym protokole PaganDE), a kliknięcia odeśle nam zdarzeniem.
61 + if let Err(err) = publish_app_menu(&mut state, &menu_labels(locale)) {
62 + tracing::warn!(error = %err, "nie udało się opublikować menu aplikacji");
63 + }
64 +
46 65 // 2. Pierwsza ramka — dopiero teraz okno staje się widoczne.
47 66 if let Err(err) = render_and_commit(&mut state, &qh) {
48 67 tracing::error!(error = %err, "nie udało się narysować pierwszej ramki");
@@ -67,6 +86,13 @@ fn main() {
67 86 handle_action(&mut state, action);
68 87 }
69 88
89 + // 4b. Kliknięcie etykiety menu aplikacji (zdarzenie z kompozytora).
90 + if let Some(index) = state.pending_menu_click.take() {
91 + let labels = menu_labels(state.locale);
92 + let label = labels.get(index).copied().unwrap_or("?");
93 + tracing::info!(index, label, "kliknięto etykietę menu aplikacji");
94 + }
95 +
70 96 // 5. Przerysowanie tylko wtedy, gdy faktycznie coś się zmieniło (hover, configure, akcja).
71 97 if state.needs_redraw && state.configured {
72 98 if let Err(err) = render_and_commit(&mut state, &qh) {
73 99 new file mode 100644
@@ -0,0 +1,105 @@
1 +//! Klient własnego protokołu `pagan_app_menu_v1` — menu aplikacji w pasku SSD.
2 +//!
3 +//! PO CO: kompozytor PaganDE rysuje pasek tytułu po swojej stronie (wymuszony
4 +//! SSD). Żeby w tym pasku pokazać menu KONKRETNEJ aplikacji, klient publikuje
5 +//! same ETYKIETY górnego poziomu, a kompozytor odsyła zdarzenie `clicked`,
6 +//! gdy użytkownik w którąś kliknie. Zawartość rozwinięcia zna i pokazuje klient.
7 +//!
8 +//! Dzięki temu każde menu może być inne (bo pochodzi od aplikacji), a mimo to
9 +//! wygląda spójnie — rysuje je ten sam pasek PaganDE.
10 +
11 +/// Kod wygenerowany z `protocols/pagan-app-menu-v1.xml` (strona klienta).
12 +pub mod protocol {
13 + #[allow(clippy::all)]
14 + pub mod client {
15 + // Wygenerowany kod odwołuje się do crate'ów przez `super::wayland_client`
16 + // oraz do interfejsów rdzenia (np. `wl_surface`) — oba musimy mieć w zasięgu.
17 + #[allow(unused_imports)]
18 + use wayland_client;
19 + #[allow(unused_imports)]
20 + use wayland_client::protocol::*;
21 +
22 + pub mod __interfaces {
23 + use wayland_client::protocol::__interfaces::*;
24 + wayland_scanner::generate_interfaces!("../protocols/pagan-app-menu-v1.xml");
25 + }
26 + use self::__interfaces::*;
27 +
28 + wayland_scanner::generate_client_code!("../protocols/pagan-app-menu-v1.xml");
29 + }
30 +}
31 +
32 +pub use protocol::client::pagan_app_menu_manager_v1::PaganAppMenuManagerV1;
33 +pub use protocol::client::pagan_app_menu_v1::{Event as AppMenuEvent, PaganAppMenuV1};
34 +
35 +use wayland_client::{Connection, Dispatch, QueueHandle};
36 +
37 +use crate::connection::CompositorState;
38 +
39 +/// Zamienia listę etykiet na format protokołu (rozdzielony znakami nowej linii).
40 +///
41 +/// PO CO tak, a nie tablica: napis jest prostszy w oby dwóch bindingach i w pełni
42 +/// wystarcza — etykiety menu nie zawierają znaków nowej linii.
43 +pub fn encode_labels(labels: &[&str]) -> String {
44 + labels.join("\n")
45 +}
46 +
47 +#[cfg(test)]
48 +mod tests {
49 + use super::{encode_labels, AppMenuEvent};
50 +
51 + #[test]
52 + fn kodowanie_etykiet_jest_odwracalne() {
53 + let labels = ["Plik", "Edycja", "Widok", "Pomoc"];
54 + let encoded = encode_labels(&labels);
55 + assert_eq!(encoded, "Plik\nEdycja\nWidok\nPomoc");
56 + // Po stronie kompozytora ten sam napis jest dzielony z powrotem.
57 + let decoded: Vec<&str> = encoded.split('\n').collect();
58 + assert_eq!(decoded, labels);
59 + }
60 +
61 + #[test]
62 + fn pusta_lista_daje_pusty_napis() {
63 + assert!(encode_labels(&[]).is_empty());
64 + }
65 +
66 + /// Zdarzenie `clicked` nie może zostać pomylone z żadnym innym.
67 + #[test]
68 + fn zdarzenie_klikniecia_niesie_indeks() {
69 + let event = AppMenuEvent::Clicked { index: 2 };
70 + match event {
71 + AppMenuEvent::Clicked { index } => assert_eq!(index, 2),
72 + }
73 + }
74 +}
75 +
76 +// Menedżer nie ma zdarzeń — wystarczy puste `Dispatch`, żeby wayland-client
77 +// wiedział, dokąd kierować ewentualne komunikaty.
78 +impl Dispatch<PaganAppMenuManagerV1, ()> for CompositorState {
79 + fn event(
80 + _state: &mut Self,
81 + _proxy: &PaganAppMenuManagerV1,
82 + _event: <PaganAppMenuManagerV1 as wayland_client::Proxy>::Event,
83 + _data: &(),
84 + _conn: &Connection,
85 + _qh: &QueueHandle<Self>,
86 + ) {
87 + }
88 +}
89 +
90 +impl Dispatch<PaganAppMenuV1, ()> for CompositorState {
91 + fn event(
92 + state: &mut Self,
93 + _proxy: &PaganAppMenuV1,
94 + event: AppMenuEvent,
95 + _data: &(),
96 + _conn: &Connection,
97 + _qh: &QueueHandle<Self>,
98 + ) {
99 + let AppMenuEvent::Clicked { index } = event;
100 + // Kliknięcie obsługuje APLIKACJA (ona zna zawartość swojego menu).
101 + // Zapamiętujemy indeks; pętla główna odbierze go z `pending_menu_click`.
102 + state.pending_menu_click = Some(index as usize);
103 + tracing::debug!(index, "kompozytor zgłosił kliknięcie etykiety menu");
104 + }
105 +}
@@ -25,6 +25,7 @@ use wayland_protocols::xdg::shell::client::{
25 25 xdg_wm_base::{self, XdgWmBase},
26 26 };
27 27
28 +use crate::app_menu::{encode_labels, PaganAppMenuManagerV1, PaganAppMenuV1};
28 29 use crate::i18n::Locale;
29 30 use crate::shm::{ShmBuffer, ShmError};
30 31 use crate::window::{MyToolkitWindow, WindowAction};
@@ -145,6 +146,14 @@ pub struct CompositorState {
145 146 /// `zxdg_toplevel_decoration_v1`.
146 147 pub decoration: Option<ZxdgToplevelDecorationV1>,
147 148
149 + // --- menu aplikacji (własny protokół PaganDE) ---
150 + /// `pagan_app_menu_manager_v1` (opcjonalny — tylko kompozytory PaganDE).
151 + pub app_menu_manager: Option<PaganAppMenuManagerV1>,
152 + /// `pagan_app_menu_v1` dla naszego okna.
153 + pub app_menu: Option<PaganAppMenuV1>,
154 + /// Indeks etykiety menu klikniętej przez użytkownika (do odebrania w pętli).
155 + pub pending_menu_click: Option<usize>,
156 +
148 157 // --- bufor ---
149 158 /// Aktualnie używany bufor SHM (tworzony leniwie, dopasowany do rozmiaru okna).
150 159 pub shm_buffer: Option<ShmBuffer>,
@@ -348,6 +357,9 @@ pub fn create_window(
348 357 let seat: wl_seat::WlSeat = globals.bind(&qh, 1..=5, ())?;
349 358 // Dekoracje są opcjonalne — brak globala nie jest błędem.
350 359 let decoration_manager: Option<ZxdgDecorationManagerV1> = globals.bind(&qh, 1..=1, ()).ok();
360 + // Menu aplikacji (własny protokół PaganDE) też jest opcjonalne — na obcym
361 + // kompozytorze po prostu nie mamy menu w pasku tytułu.
362 + let app_menu_manager: Option<PaganAppMenuManagerV1> = globals.bind(&qh, 1..=1, ()).ok();
351 363
352 364 // Kolejność jest wymuszona protokołem: surface → xdg_surface → rola (xdg_toplevel).
353 365 let surface = compositor.create_surface(&qh, ());
@@ -372,6 +384,15 @@ pub fn create_window(
372 384 tracing::info!("kompozytor nie oferuje zxdg_decoration_manager_v1 — pozostajemy przy CSD");
373 385 }
374 386
387 + // Menu aplikacji: obiekt tworzymy od razu, ale etykiety opublikuje aplikacja
388 + // dopiero przez [`publish_app_menu`] (bo tylko ona wie, jakie ma menu).
389 + let app_menu = app_menu_manager
390 + .as_ref()
391 + .map(|manager| manager.get_app_menu(&surface, &qh, ()));
392 + if app_menu.is_none() {
393 + tracing::info!("kompozytor nie oferuje pagan_app_menu_manager_v1 — brak menu w pasku SSD");
394 + }
395 +
375 396 let mut state = CompositorState {
376 397 connection: connection.clone(),
377 398 window: MyToolkitWindow::with_locale(width, height, title, locale),
@@ -386,6 +407,9 @@ pub fn create_window(
386 407 xdg_surface: Some(xdg_surface),
387 408 toplevel: Some(toplevel),
388 409 decoration,
410 + app_menu_manager,
411 + app_menu,
412 + pending_menu_click: None,
389 413 shm_buffer: None,
390 414 configured: false,
391 415 running: true,
@@ -480,3 +504,24 @@ pub fn render_and_commit(
480 504
481 505 Ok(())
482 506 }
507 +
508 +/// Publikuje etykiety menu aplikacji w pasku tytułu (własny protokół PaganDE).
509 +///
510 +/// PO CO osobna funkcja: aplikacja wywołuje ją, gdy zna już swoje menu — a to
511 +/// może zależeć od kontekstu (np. inny zestaw pozycji w trybie edycji). Kompozytor
512 +/// narysuje je w firmowym pasku, a kliknięcia odeśle zdarzeniem `clicked`.
513 +///
514 +/// Brak globala `pagan_app_menu_manager_v1` nie jest błędem — na obcym
515 +/// kompozytorze po prostu nie ma gdzie pokazać menu.
516 +pub fn publish_app_menu(state: &mut CompositorState, labels: &[&str]) -> Result<(), ToolkitError> {
517 + let Some(menu) = state.app_menu.as_ref() else {
518 + return Ok(());
519 + };
520 + menu.set_labels(encode_labels(labels));
521 + // Wypychamy od razu, żeby pasek zdążył się zaktualizować przed pierwszą klatką.
522 + state
523 + .connection
524 + .flush()
525 + .map_err(|err| ToolkitError::Wayland(err.to_string()))?;
526 + Ok(())
527 +}
@@ -24,6 +24,7 @@
24 24 //! # Ok::<(), Box<dyn std::error::Error>>(())
25 25 //! ```
26 26
27 +pub mod app_menu;
27 28 pub mod connection;
28 29 pub mod draw;
29 30 pub mod i18n;
@@ -32,7 +33,9 @@ pub mod shm;
32 33 pub mod window;
33 34
34 35 // Re-exporty publicznego API — dzięki nim użytkownik nie musi znać rozkładu modułów.
35 -pub use connection::{create_window, render_and_commit, CompositorState, ToolkitError};
36 +pub use connection::{
37 + create_window, publish_app_menu, render_and_commit, CompositorState, ToolkitError,
38 +};
36 39 pub use i18n::{tr, Locale, MsgKey};
37 40 pub use shm::{ShmBuffer, ShmError};
38 41 pub use window::{Edges, MyToolkitWindow, WindowAction};
39 42 new file mode 100644
@@ -0,0 +1,95 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<protocol name="pagan_app_menu_v1">
3 + <copyright>
4 + Copyright (c) 2026 PaganDE
5 +
6 + Permission is hereby granted, free of charge, to any person obtaining a
7 + copy of this software and associated documentation files (the "Software"),
8 + to deal in the Software without restriction, including without limitation
9 + the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 + and/or sell copies of the Software, and to permit persons to whom the
11 + Software is furnished to do so, subject to the following conditions:
12 +
13 + The above copyright notice and this permission notice (including the next
14 + paragraph) shall be included in all copies or substantial portions of the
15 + Software.
16 +
17 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
20 + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
21 + OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22 + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23 + OTHER DEALINGS IN THE SOFTWARE.
24 + </copyright>
25 +
26 + <description summary="menu aplikacji w dekoracjach serwerowych PaganDE">
27 + Protokół pozwala klientowi opublikować ETYKIETY swojego głównego menu
28 + (np. Plik, Edycja, Widok, Pomoc), aby kompozytor mógł narysować je
29 + w pasku tytułu rysowanym po stronie serwera (SSD) — w tym samym rzędzie
30 + co przyciski minimalizuj/maksymalizuj/zamknij.
31 +
32 + Podział odpowiedzialności jest celowy:
33 +
34 + * KOMPOZYTOR rysuje tylko górny poziom menu (etykiety) i informuje klienta,
35 + która etykieta została kliknięta,
36 + * KLIENT zna ZAWARTOŚĆ swojego menu (pozycje, skróty, stany) i pokazuje
37 + rozwinięcie po otrzymaniu zdarzenia `clicked`.
38 +
39 + Dzięki temu każde menu może być inne (bo pochodzi od aplikacji), a mimo to
40 + wygląda spójnie, bo jest rysowane w firmowym pasku PaganDE.
41 +
42 + Uwaga: to rozszerzenie jest specyficzne dla PaganDE.
43 + </description>
44 +
45 + <interface name="pagan_app_menu_manager_v1" version="1">
46 + <description summary="globalna fabryka menu aplikacji">
47 + Punkt wejścia protokołu. Klient pobiera z niego obiekt menu dla swojej
48 + powierzchni najwyższego poziomu.
49 + </description>
50 +
51 + <request name="get_app_menu">
52 + <description summary="utwórz obiekt menu dla powierzchni">
53 + Tworzy obiekt menu związany z powierzchnią `surface`. Jeśli klient
54 + utworzy kilka obiektów dla tej samej powierzchni, obowiązuje ostatni
55 + ustawiony zestaw etykiet.
56 + </description>
57 + <arg name="id" type="new_id" interface="pagan_app_menu_v1"/>
58 + <arg name="surface" type="object" interface="wl_surface"/>
59 + </request>
60 +
61 + <request name="destroy" type="destructor">
62 + <description summary="zniszcz obiekt fabryki"/>
63 + </request>
64 + </interface>
65 +
66 + <interface name="pagan_app_menu_v1" version="1">
67 + <description summary="menu aplikacji przypisane do powierzchni">
68 + Reprezentuje górny poziom menu aplikacji. Etykiety są rozdzielone
69 + znakiem nowej linii ("\n"); pusta wartość oznacza brak menu
70 + (kompozytor pokaże wtedy pojedynczy przycisk „burger”).
71 + </description>
72 +
73 + <request name="set_labels">
74 + <description summary="ustaw etykiety górnego poziomu menu">
75 + Ustawia (zastępuje) listę etykiet menu. Etykiety są rozdzielone znakiem
76 + nowej linii. Kolejność odpowiada indeksom w zdarzeniu `clicked`.
77 + Wysłanie pustego napisu usuwa menu.
78 + </description>
79 + <arg name="labels" type="string"/>
80 + </request>
81 +
82 + <request name="destroy" type="destructor">
83 + <description summary="zniszcz obiekt menu"/>
84 + </request>
85 +
86 + <event name="clicked">
87 + <description summary="użytkownik kliknął etykietę menu">
88 + Kompozytor informuje, że użytkownik kliknął etykietę o podanym
89 + indeksie (0 = pierwsza etykieta z ostatniego `set_labels`). Klient
90 + powinien wtedy pokazać zawartość tego menu.
91 + </description>
92 + <arg name="index" type="uint"/>
93 + </event>
94 + </interface>
95 +</protocol>