← PaganDE

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() {