🔒 Repository is read-only – file editing is disabled.

PaganDE/pagan-toolkit/examples/demo.rs main

219 linii Raw ← Powrót
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();
}