//! Demo `pagan-toolkit`: okno z dekoracjami serwerowymi (SSD) i działającym menu. //! //! Uruchomienie (na kompozytorze PaganDE lub dowolnym innym kompozytorze Wayland): //! //! ```sh //! WAYLAND_DISPLAY=wayland-1 PAGAN_LANG=pl \ //! cargo run --example demo //! ``` //! //! Co pokazuje to demo: //! * połączenie z kompozytorem i role xdg-shell (`create_window`), //! * **menu aplikacji w pasku SSD**: publikujemy tytuły własnym protokołem //! `pagan_app_menu_v1`, kompozytor rysuje je obok przycisków okna i odsyła //! kliknięcia (`pending_menu_click`), //! * **rozwijane menu** rysowane przez klienta (`OpenMenu`): klik w pozycję //! kończy się akcją (`WindowAction::MenuItem`), //! * renderowanie do bufora SHM i wysłanie ramki (`render_and_commit`), //! * lokalizację napisów przez `PAGAN_LANG` (PL/EN). use tracing_subscriber::EnvFilter; use pagan_toolkit::connection::{create_window, publish_app_menu, render_and_commit}; use pagan_toolkit::i18n::{tr, Locale, MsgKey}; use pagan_toolkit::window::WindowAction; use pagan_toolkit::{AppMenu, CompositorState}; /// Rozmiar okna demo. Bufor SHM ma dokładnie ten rozmiar (4 bajty/piksel). const WINDOW_WIDTH: i32 = 800; const WINDOW_HEIGHT: i32 = 500; /// Definicja menu aplikacji (tytuły + pozycje), lokalizowana przez `PAGAN_LANG`. /// /// PO CO jedno miejsce: te same tytuły publikujemy kompozytorowi (rysuje je /// w pasku SSD), a pozycje tworzą rozwinięcie pokazywane po kliknięciu. fn app_menu(locale: Locale) -> AppMenu { let mut menu = AppMenu::new(); match locale { Locale::Pl => { menu.add( tr(locale, MsgKey::MenuFile), &["Nowe okno", "Otwórz…", "Zapisz", "Zamknij"], ) .add(tr(locale, MsgKey::MenuEdit), &["Wytnij", "Kopiuj", "Wklej"]) .add( tr(locale, MsgKey::MenuView), &["Powiększ", "Pomniejsz", "Pełny ekran"], ) .add(tr(locale, MsgKey::MenuHelp), &["O programie"]); } Locale::En => { menu.add( tr(locale, MsgKey::MenuFile), &["New window", "Open…", "Save", "Quit"], ) .add(tr(locale, MsgKey::MenuEdit), &["Cut", "Copy", "Paste"]) .add( tr(locale, MsgKey::MenuView), &["Zoom in", "Zoom out", "Fullscreen"], ) .add(tr(locale, MsgKey::MenuHelp), &["About"]); } } menu } fn main() { init_logging(); let locale = Locale::from_env(); let title = tr(locale, MsgKey::WindowTitle); tracing::info!(?locale, "start Pagan Toolkit demo"); // 1. Połączenie + role xdg-shell + pierwszy configure (bufor jeszcze niepodłączony). let (mut event_queue, mut state) = match create_window(WINDOW_WIDTH, WINDOW_HEIGHT, title, locale) { Ok(pair) => pair, Err(err) => { tracing::error!(error = %err, "nie udało się utworzyć okna"); std::process::exit(1); } }; let qh = event_queue.handle(); // 1b. Publikujemy menu aplikacji kompozytorowi — narysuje tytuły w pasku // tytułu, a kliknięcia odeśle nam zdarzeniem `clicked`. let menu = app_menu(locale); if let Err(err) = publish_app_menu(&mut state, &menu) { tracing::warn!(error = %err, "nie udało się opublikować menu aplikacji"); } // 2. Pierwsza ramka — dopiero teraz okno staje się widoczne. if let Err(err) = render_and_commit(&mut state, &qh) { tracing::error!(error = %err, "nie udało się narysować pierwszej ramki"); } if let Err(err) = event_queue.flush() { tracing::error!(error = %err, "nie udało się wypchnąć żądań do kompozytora"); } // Numer tytułu menu, którego rozwinięcie jest aktualnie otwarte (jeśli któreś). let mut open_menu: Option = None; // 3. Pętla zdarzeń. `blocking_dispatch` blokuje do najbliższego zdarzenia, // więc gdy nic się nie dzieje, proces nie zajmuje CPU. while state.running { match event_queue.blocking_dispatch(&mut state) { Ok(count) => tracing::trace!(count, "obsłużono zdarzenia"), Err(err) => { tracing::error!(error = %err, "błąd dyspozycji zdarzeń — kończę pętlę"); break; } } // 4. Reakcja na akcję wyprodukowaną przez wejście (patrz `input.rs`). if let Some(action) = state.pending_action.take() { handle_action(&mut state, action, &mut open_menu); } // 5. Kliknięcie etykiety menu w pasku SSD (zdarzenie z kompozytora). if let Some((index, _x)) = state.pending_menu_click.take() { open_menu = Some(index); tracing::info!(index, "otwarto menu z paska tytułu"); } // 6. Przerysowanie tylko wtedy, gdy faktycznie coś się zmieniło (hover, configure, akcja). if state.needs_redraw && state.configured { if let Err(err) = render_and_commit(&mut state, &qh) { tracing::error!(error = %err, "nie udało się przerysować okna"); } if let Err(err) = event_queue.flush() { tracing::error!(error = %err, "nie udało się wypchnąć żądań do kompozytora"); } state.needs_redraw = false; } } tracing::info!("{}", tr(state.locale, MsgKey::LogExit)); } /// Wykonuje akcję z `WindowAction` i aktualizuje stan aplikacji. /// /// Uwaga: `StartMove` i `resize` zostały już przekazane kompozytorowi w `input.rs` /// (bo tylko tam mieliśmy świeży serial). Tutaj tylko logujemy i ewentualnie /// zmieniamy własny stan (np. maksymalizację albo zamknięcie okna). fn handle_action(state: &mut CompositorState, action: WindowAction, open_menu: &mut Option) { match action { WindowAction::Close => { // Logujemy w wybranym języku; pętlę zakończy po sprawdzeniu `state.running`. tracing::info!("{}", tr(state.locale, MsgKey::LogClosing)); state.running = false; } WindowAction::Minimize => { tracing::info!("{}", tr(state.locale, MsgKey::Minimize)); if let Some(toplevel) = state.toplevel.as_ref() { toplevel.set_minimized(); } } WindowAction::Maximize => { // Przełączamy stan i prosimy kompozytor; przyjdzie configure z nowym rozmiarem. let target = !state.maximized; if let Some(toplevel) = state.toplevel.as_ref() { if target { toplevel.set_maximized(); } else { toplevel.unset_maximized(); } } state.maximized = target; state.needs_redraw = true; tracing::info!("{}", tr(state.locale, MsgKey::LogMaximize)); } WindowAction::ShowMenu => { // Wywoływane tylko w trybie CSD (na obcym kompozytorze) — u nas menu // jest w pasku SSD, więc ten przycisk zwykle nie występuje. tracing::info!("{}", tr(state.locale, MsgKey::LogMenu)); } WindowAction::MenuItem(item_index) => { let menu_index = open_menu.take(); if let Some(menu_index) = menu_index { run_menu_action(state, menu_index, item_index); } } WindowAction::StartMove => { tracing::info!("{}", tr(state.locale, MsgKey::LogDrag)); } WindowAction::None => {} } } /// Wykonuje akcję wybranej pozycji menu. /// /// PO CO osobno: pokazuje pełny łańcuch działania — kompozytor rysuje tytuły, /// klient rozwija pozycje, a wybór zamienia się na konkretną akcję aplikacji. fn run_menu_action(state: &mut CompositorState, menu_index: usize, item_index: usize) { let label = state .app_menu_def .items(menu_index) .get(item_index) .cloned() .unwrap_or_default(); tracing::info!( menu_index, item_index, label, "wybrano pozycję menu aplikacji" ); // „Zamknij" to ostatnia pozycja menu „Plik" — najprostszy test, że akcja // naprawdę dociera do aplikacji. if menu_index == 0 && item_index == 3 { tracing::info!("{}", tr(state.locale, MsgKey::LogClosing)); state.running = false; } } /// Konfiguruje `tracing-subscriber`; domyślnie poziom `info`, nadpisywalny przez `RUST_LOG`. fn init_logging() { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::fmt().with_env_filter(filter).init(); }