Commit 970b605
4
plików
+192
dodanych
-74
usuniętych
@@ -24,7 +24,7 @@ aplikacje mają różne menu. Rozwiązuje to **własny protokół** `pagan_app_m
24
24
klient publikuje etykiety górnego poziomu, kompozytor rysuje je w pasku i odsyła
25
25
kliknięcia. Szczegóły: [Dekoracje serwerowe (SSD)](#dekoracje-serwerowe-ssd-i-menu-aplikacji-w-pasku).
26
26
27
-Aplikacje bez tego protokołu dostają w pasku przycisk „burger”.
27
+Aplikacje bez tego protokołu dostają w pasku przycisk „burger” (zamiast etykiet).
28
28
29
29
## Wymagania
30
30
@@ -193,8 +193,11 @@ menu.add("Plik", &["Nowe okno", "Otwórz…", "Zapisz", "Zamknij"])
193
193
pagan_toolkit::publish_app_menu(&mut state, &menu)?;
194
194
```
195
195
196
-Aplikacje, które tego protokołu nie używają (np. terminale X11), dostają w pasku
197
-pojedynczy przycisk „burger”, rozwijający menu okna.
196
+Aplikacje bez tego protokołu dostają w pasku przycisk „burger”. Gdy okno jest
197
+za wąskie na etykiety (`ssd_menu_min_width`), etykiety zwijają się do burgera —
198
+a jego **rozwinięcie to menu aplikacji** (wiersze = etykiety, klik odsyłamy
199
+klientowi). Dopiero gdy klient nie ma menu (np. terminale X11), burger rozwija
200
+awaryjne menu okna (minimalizuj/maksymalizuj/zamknij).
198
201
199
202
Tekst na pasku (tytuł i etykiety menu) rysuje kompozytor: rasteryzuje go Cairo
200
203
i wysyła na GPU jako teksturę (`pagan-compositor/src/text.rs`, cache per napis).
@@ -12,8 +12,11 @@
12
12
//! Dzięki temu wygląd jest spójny (rysuje go kompozytor), a funkcje menu należą
13
13
//! do aplikacji.
14
14
//!
15
-//! Aplikacje, które tego protokołu nie używają (obce), dostają w pasku pojedynczy
16
-//! przycisk „burger” zamiast etykiet.
15
+//! Gdy w pasku nie ma miejsca na etykiety (wąskie okno), zwijają się one do
16
+//! jednego przycisku „burger”. Rozwinięcie burgera pokazuje wtedy WŁAŚNIE menu
17
+//! aplikacji (klik wiersza odsyłamy klientowi). Dopiero gdy klient nie ma menu
18
+//! (obce aplikacje, np. terminale X11), burger rozwija awaryjne menu okna
19
+//! (minimalizuj/maksymalizuj/zamknij).
17
20
18
21
use std::sync::Mutex;
19
22
@@ -37,7 +37,7 @@ use crate::{
37
37
focus::{KeyboardFocusTarget, PointerFocusTarget},
38
38
grabs::PointerMoveSurfaceGrab,
39
39
state::MyCompositor,
40
- window::{PaganWindow, TitlebarButton},
40
+ window::{PaganWindow, SsdMenuKind, TitlebarButton},
41
41
};
42
42
43
43
impl MyCompositor {
@@ -359,23 +359,35 @@ impl MyCompositor {
359
359
fn handle_ssd_press(&mut self, button: u32, serial: Serial) -> bool {
360
360
let point = self.pointer_location;
361
361
362
- // 1) Klik w rozwinięte menu okna — wykonaj akcję wiersza i zamknij menu.
362
+ // 1) Klik w rozwinięte menu okna — zależnie od rodzaju menu wykonaj akcję
363
+ // okna albo przekaż klik klientowi (menu aplikacji).
363
364
if let Some((window, location)) = self.ssd_menu_hit(point) {
364
365
self.raise_and_focus(&window, serial);
366
+ let kind = window.ssd_menu_kind();
365
367
let row = window.ssd_menu_row_at(location, point);
368
+ let label_row = window.ssd_menu_label_row_at(location, point);
366
369
window.set_ssd_menu_open(false);
367
- match row {
368
- Some(TitlebarButton::Minimize) => self.minimize_window(&window),
369
- Some(TitlebarButton::Maximize) => {
370
- if window.is_maximized() {
371
- self.unmaximize_window(&window);
372
- } else {
373
- self.maximize_window(&window);
370
+ match kind {
371
+ // Menu aplikacji: klik wiersza odsyłamy KLIENTOWI — tylko on zna
372
+ // zawartość rozwinięcia i pokaże je pod właściwą etykietą.
373
+ SsdMenuKind::AppMenu => {
374
+ if let Some(index) = label_row {
375
+ crate::app_menu::send_click(&window, location, index);
374
376
}
375
377
}
376
- Some(TitlebarButton::Close) => self.close_window(&window),
377
- // Klik w tło panelu (poza wierszem) tylko zamyka menu.
378
- _ => {}
378
+ SsdMenuKind::Window => match row {
379
+ Some(TitlebarButton::Minimize) => self.minimize_window(&window),
380
+ Some(TitlebarButton::Maximize) => {
381
+ if window.is_maximized() {
382
+ self.unmaximize_window(&window);
383
+ } else {
384
+ self.maximize_window(&window);
385
+ }
386
+ }
387
+ Some(TitlebarButton::Close) => self.close_window(&window),
388
+ // Klik w tło panelu (poza wierszem) tylko zamyka menu.
389
+ _ => {}
390
+ },
379
391
}
380
392
return true;
381
393
}
@@ -81,6 +81,8 @@ const MENU_ROWS: i32 = 3;
81
81
const MENU_ROW_ICON_X: i32 = 24;
82
82
/// Odstęp napisu wiersza menu od jego ikony (px).
83
83
const MENU_ROW_LABEL_OFFSET: i32 = 18;
84
+/// Lewy margines napisu w wierszu MENU APLIKACJI (bez ikony, px).
85
+const MENU_LABEL_ROW_PAD_X: i32 = 16;
84
86
85
87
/// Który przycisk paska tytułu trafiono.
86
88
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -92,6 +94,21 @@ pub enum TitlebarButton {
92
94
Close,
93
95
}
94
96
97
+/// Co pokazuje rozwinięcie spod burgera (gdy w pasku nie ma miejsca na etykiety).
98
+///
99
+/// PO CO rozróżniamy: menu APLIKACJI (Plik/Edycja/…, publikowane przez klienta)
100
+/// to coś innego niż menu OKNA (minimalizuj/maksymalizuj/zamknij). Akcje okna
101
+/// mamy już jako przyciski po prawej, więc burger bez własnego menu klienta jest
102
+/// tylko awaryjnym menu okna — a nie duplikatem przycisków.
103
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104
+pub enum SsdMenuKind {
105
+ /// Menu aplikacji: etykiety górnego poziomu od klienta. Klik odsyłamy
106
+ /// klientowi (`clicked`), bo tylko on zna zawartość rozwinięcia.
107
+ AppMenu,
108
+ /// Menu okna dla klientów bez własnego menu (np. terminale X11).
109
+ Window,
110
+}
111
+
95
112
/// Stan dekoracji okna trzymany w danych użytkownika okna.
96
113
///
97
114
/// PO CO w danych okna, a nie w `MyCompositor`: dekorację rysujemy razem z oknem
@@ -382,8 +399,32 @@ impl PaganWindow {
382
399
}
383
400
384
401
/// Prostokąt panelu rozwiniętego menu okna (pod burgerem).
402
+ ///
403
+ /// Wysokość zależy od zawartości: menu aplikacji ma tyle wierszy, ile etykiet
404
+ /// opublikował klient, a menu okna zawsze trzy (minimalizuj/maksymalizuj/zamknij).
385
405
pub fn ssd_menu_rect(&self, location: Point<i32, Logical>) -> Rectangle<i32, Logical> {
386
- menu_panel_rect(self.titlebar_rect(location))
406
+ menu_panel_rect(self.titlebar_rect(location), self.menu_row_count())
407
+ }
408
+
409
+ /// Co pokazuje rozwinięcie spod burgera (patrz `SsdMenuKind`).
410
+ ///
411
+ /// Skoro burger pojawia się tylko wtedy, gdy w pasku brakuje miejsca na
412
+ /// etykiety, rozwinięcie pokazuje menu APLIKACJI dokładnie wtedy, gdy klient
413
+ /// je opublikował — a menu okna tylko jako awaryjne dla obcych aplikacji.
414
+ pub fn ssd_menu_kind(&self) -> SsdMenuKind {
415
+ if crate::app_menu::labels(self).is_empty() {
416
+ SsdMenuKind::Window
417
+ } else {
418
+ SsdMenuKind::AppMenu
419
+ }
420
+ }
421
+
422
+ /// Liczba wierszy rozwiniętego menu (używana też do wyliczenia wysokości panelu).
423
+ fn menu_row_count(&self) -> i32 {
424
+ match self.ssd_menu_kind() {
425
+ SsdMenuKind::AppMenu => crate::app_menu::labels(self).len().max(1) as i32,
426
+ SsdMenuKind::Window => MENU_ROWS,
427
+ }
387
428
}
388
429
389
430
/// Czy punkt leży wewnątrz panelu menu (niezależnie od wiersza).
@@ -395,18 +436,41 @@ impl PaganWindow {
395
436
self.ssd_menu_open() && self.ssd_menu_rect(location).to_f64().contains(point)
396
437
}
397
438
398
- /// Który wiersz menu trafiono (minimalizuj / maksymalizuj / zamknij).
439
+ /// Który wiersz menu OKNA trafiono (minimalizuj / maksymalizuj / zamknij).
399
440
pub fn ssd_menu_row_at(
400
441
&self,
401
442
location: Point<i32, Logical>,
402
443
point: Point<f64, Logical>,
403
444
) -> Option<TitlebarButton> {
404
- if !self.ssd_menu_open() {
445
+ if !self.ssd_menu_open() || self.ssd_menu_kind() != SsdMenuKind::Window {
405
446
return None;
406
447
}
407
448
menu_row_at(self.ssd_menu_rect(location), point)
408
449
}
409
450
451
+ /// Indeks etykiety MENU APLIKACJI trafionej w rozwinięciu (nie w pasku).
452
+ ///
453
+ /// PO CO osobno od `ssd_menu_row_at`: wiersze menu aplikacji odpowiadają
454
+ /// etykietom klienta, więc klik przekazujemy klientowi, a nie wykonujemy
455
+ /// akcji okna.
456
+ pub fn ssd_menu_label_row_at(
457
+ &self,
458
+ location: Point<i32, Logical>,
459
+ point: Point<f64, Logical>,
460
+ ) -> Option<usize> {
461
+ if !self.ssd_menu_open() || self.ssd_menu_kind() != SsdMenuKind::AppMenu {
462
+ return None;
463
+ }
464
+ let panel = self.ssd_menu_rect(location);
465
+ if !panel.to_f64().contains(point) {
466
+ return None;
467
+ }
468
+ let row =
469
+ ((point.y as i32 - panel.loc.y - MENU_PANEL_PADDING) / MENU_ROW_HEIGHT).max(0) as usize;
470
+ let count = crate::app_menu::labels(self).len();
471
+ (row < count).then_some(row)
472
+ }
473
+
410
474
/// Czy punkt (w układzie przestrzeni) leży na pasku tytułu tego okna.
411
475
pub fn titlebar_contains(
412
476
&self,
@@ -476,13 +540,13 @@ fn window_buttons(bar: Rectangle<i32, Logical>) -> [(TitlebarButton, Point<i32,
476
540
]
477
541
}
478
542
479
-/// Panel rozwijanego menu okna (tuż pod belką paska).
480
-fn menu_panel_rect(bar: Rectangle<i32, Logical>) -> Rectangle<i32, Logical> {
543
+/// Panel rozwijanego menu (tuż pod belką paska). `rows` = liczba wierszy.
544
+fn menu_panel_rect(bar: Rectangle<i32, Logical>, rows: i32) -> Rectangle<i32, Logical> {
481
545
Rectangle::new(
482
546
Point::from((bar.loc.x + MENU_PANEL_PADDING, bar.loc.y + TITLEBAR_HEIGHT)),
483
547
Size::from((
484
548
MENU_PANEL_WIDTH,
485
- MENU_PANEL_PADDING * 2 + MENU_ROW_HEIGHT * MENU_ROWS,
549
+ MENU_PANEL_PADDING * 2 + MENU_ROW_HEIGHT * rows.max(1),
486
550
)),
487
551
)
488
552
}
@@ -670,54 +734,80 @@ impl AsRenderElements<GlesRenderer> for PaganWindow {
670
734
// bo jest nierozerwalnie związane z konkretnym oknem i jego Z-orderem.
671
735
if self.wants_ssd() && self.ssd_menu_open() {
672
736
let panel = self.ssd_menu_rect(logical_loc);
673
- // Kolejność: NAJPIERW glify wierszy, POTEM tło panelu — lista jest
674
- // front-to-back, więc tło (wypchnięte później) wyląduje POD glifami.
675
- for (row, button) in [
676
- TitlebarButton::Minimize,
677
- TitlebarButton::Maximize,
678
- TitlebarButton::Close,
679
- ]
680
- .into_iter()
681
- .enumerate()
682
- {
683
- let center = Point::from((
684
- panel.loc.x + MENU_ROW_ICON_X,
685
- panel.loc.y
686
- + MENU_PANEL_PADDING
687
- + MENU_ROW_HEIGHT * row as i32
688
- + MENU_ROW_HEIGHT / 2,
689
- ));
690
- push_icon(
691
- &mut elements,
692
- renderer,
693
- button,
694
- center,
695
- icon_px,
696
- if button == TitlebarButton::Close {
697
- close_rgb
698
- } else {
699
- text_rgb
700
- },
701
- scale,
702
- );
703
- let label = menu_row_label(button);
704
- let text_x = panel.loc.x + MENU_ROW_ICON_X + MENU_ROW_LABEL_OFFSET;
705
- if let Some(texture) = text::texture(label, font_px, text_rgb) {
706
- let loc = Point::<f64, Logical>::from((
707
- text_x as f64,
708
- (center.y - texture.size.h / 2) as f64,
709
- ))
710
- .to_physical(scale);
711
- if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
712
- renderer,
713
- loc,
714
- &texture.buffer,
715
- None,
716
- None,
717
- None,
718
- Kind::Unspecified,
719
- ) {
720
- elements.push(WindowRenderElement::Text(element));
737
+ // Kolejność: NAJPIERW treść wierszy, POTEM tło panelu — lista jest
738
+ // front-to-back, więc tło (wypchnięte później) wyląduje POD napisami.
739
+ match self.ssd_menu_kind() {
740
+ SsdMenuKind::AppMenu => {
741
+ // Menu APLIKACJI: same etykiety górnego poziomu. Rozwinięcie
742
+ // rysuje KLIENT — my tylko przekazujemy klik na wiersz.
743
+ for (row, label) in crate::app_menu::labels(self).into_iter().enumerate() {
744
+ let center_y = panel.loc.y
745
+ + MENU_PANEL_PADDING
746
+ + MENU_ROW_HEIGHT * row as i32
747
+ + MENU_ROW_HEIGHT / 2;
748
+ let width = text::measure(&label, font_px)
749
+ .map(|size| size.w)
750
+ .unwrap_or(0);
751
+ push_text(
752
+ &mut elements,
753
+ renderer,
754
+ &label,
755
+ font_px,
756
+ text_rgb,
757
+ Point::from((panel.loc.x + MENU_LABEL_ROW_PAD_X + width / 2, center_y)),
758
+ scale,
759
+ );
760
+ }
761
+ }
762
+ SsdMenuKind::Window => {
763
+ for (row, button) in [
764
+ TitlebarButton::Minimize,
765
+ TitlebarButton::Maximize,
766
+ TitlebarButton::Close,
767
+ ]
768
+ .into_iter()
769
+ .enumerate()
770
+ {
771
+ let center = Point::from((
772
+ panel.loc.x + MENU_ROW_ICON_X,
773
+ panel.loc.y
774
+ + MENU_PANEL_PADDING
775
+ + MENU_ROW_HEIGHT * row as i32
776
+ + MENU_ROW_HEIGHT / 2,
777
+ ));
778
+ push_icon(
779
+ &mut elements,
780
+ renderer,
781
+ button,
782
+ center,
783
+ icon_px,
784
+ if button == TitlebarButton::Close {
785
+ close_rgb
786
+ } else {
787
+ text_rgb
788
+ },
789
+ scale,
790
+ );
791
+ let label = menu_row_label(button);
792
+ let text_x = panel.loc.x + MENU_ROW_ICON_X + MENU_ROW_LABEL_OFFSET;
793
+ if let Some(texture) = text::texture(label, font_px, text_rgb) {
794
+ let loc = Point::<f64, Logical>::from((
795
+ text_x as f64,
796
+ (center.y - texture.size.h / 2) as f64,
797
+ ))
798
+ .to_physical(scale);
799
+ if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
800
+ renderer,
801
+ loc,
802
+ &texture.buffer,
803
+ None,
804
+ None,
805
+ None,
806
+ Kind::Unspecified,
807
+ ) {
808
+ elements.push(WindowRenderElement::Text(element));
809
+ }
810
+ }
721
811
}
722
812
}
723
813
}
@@ -1021,7 +1111,7 @@ mod tests {
1021
1111
#[test]
1022
1112
fn wiersze_menu_maja_stala_kolejnosc() {
1023
1113
let bar = bar_rect(Point::from((0, 40)), 600);
1024
- let panel = menu_panel_rect(bar);
1114
+ let panel = menu_panel_rect(bar, MENU_ROWS);
1025
1115
assert_eq!(
1026
1116
panel.loc,
1027
1117
Point::from((MENU_PANEL_PADDING, TITLEBAR_HEIGHT))
@@ -1052,7 +1142,7 @@ mod tests {
1052
1142
#[test]
1053
1143
fn punkt_poza_panelem_nie_trafia() {
1054
1144
let bar = bar_rect(Point::from((0, 40)), 600);
1055
- let panel = menu_panel_rect(bar);
1145
+ let panel = menu_panel_rect(bar, MENU_ROWS);
1056
1146
// Po prawej stronie panelu (poza jego szerokością).
1057
1147
let point = Point::from(((panel.loc.x + panel.size.w + 5) as f64, 100.0));
1058
1148
assert_eq!(menu_row_at(panel, point), None);
@@ -1062,7 +1152,7 @@ mod tests {
1062
1152
#[test]
1063
1153
fn panel_menu_jest_pod_belka() {
1064
1154
let bar = bar_rect(Point::from((0, 40)), 800);
1065
- let panel = menu_panel_rect(bar);
1155
+ let panel = menu_panel_rect(bar, MENU_ROWS);
1066
1156
assert_eq!(panel.loc.y, bar.loc.y + TITLEBAR_HEIGHT);
1067
1157
assert_eq!(panel.size.w, MENU_PANEL_WIDTH);
1068
1158
assert_eq!(
@@ -1071,6 +1161,16 @@ mod tests {
1071
1161
);
1072
1162
}
1073
1163
1164
+ /// Wysokość panelu menu ROŚNIE z liczbą etykiet menu aplikacji.
1165
+ #[test]
1166
+ fn panel_menu_aplikacji_ma_tyle_wierszy_ile_etykiet() {
1167
+ let bar = bar_rect(Point::from((0, 40)), 800);
1168
+ let cztery = menu_panel_rect(bar, 4);
1169
+ let dwie = menu_panel_rect(bar, 2);
1170
+ assert_eq!(cztery.size.h, MENU_PANEL_PADDING * 2 + MENU_ROW_HEIGHT * 4);
1171
+ assert!(cztery.size.h > dwie.size.h);
1172
+ }
1173
+
1074
1174
/// Etykiety menu układają się od lewej, jedna za drugą, na jednej osi Y.
1075
1175
#[test]
1076
1176
fn etykiety_menu_na_tej_samej_osi_y() {