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>