🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
//! 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<usize> = 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<usize>) {
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();
}