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

PaganDE/pagan-compositor/src/input.rs main

650 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
//! Obsługa wejścia: mysz, klawiatura, Z-order i kaskada okien.
//!
//! PO CO osobno od `state.rs`: to logika "polityki" menedżera okien, a nie
//! mechaniki protokołów. Dzięki separacji łatwiej zamienić zachowanie (np. na
//! przyszłe animacje) bez ruszania handlerów Waylanda.
//!
//! Ważne dla hybrydowego CSD: kompozytor NIE wykrywa sam paska tytułu ani
//! krawędzi. To klient (nasz toolkit) wie, gdzie są jego przyciski, i wysyła
//! `xdg_toplevel.move` / `resize`. Kompozytor odpowiada standardowym grabem
//! (patrz `grabs.rs`). Jedyna rzecz, którą robimy przy kliknięciu, to
//! podniesienie okna w Z-order i nadanie mu fokusu — bo to jest rola
//! menedżera okien.

use std::convert::TryInto;
use std::sync::atomic::Ordering;

use smithay::{
    backend::input::{
        AbsolutePositionEvent, Axis, AxisSource, Event, InputBackend, InputEvent, KeyState,
        KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
    },
    desktop::{layer_map_for_output, WindowSurfaceType},
    input::{
        keyboard::{FilterResult, Keysym, KeysymHandle, ModifiersState},
        pointer::{
            AxisFrame, ButtonEvent, Focus, GrabStartData as PointerGrabStartData, MotionEvent,
            RelativeMotionEvent,
        },
    },
    output::Output,
    reexports::wayland_server::protocol::wl_pointer,
    utils::{Logical, Point, Serial, SERIAL_COUNTER as SCOUNTER},
    wayland::shell::wlr_layer::Layer as WlrLayer,
};

use crate::{
    focus::{KeyboardFocusTarget, PointerFocusTarget},
    grabs::PointerMoveSurfaceGrab,
    state::MyCompositor,
    window::{PaganWindow, SsdMenuKind, TitlebarButton},
};

impl MyCompositor {
    /// Zwraca powierzchnię pod danym punktem (w przestrzeni logicznej).
    ///
    /// PO CO: Smithay potrzebuje celu, do którego wyśle ruch/klik. Iterujemy
    /// przestrzeń od wierzchu (Z-order malejąco), więc górne okno wygrywa.
    pub fn surface_under(
        &self,
        pos: Point<f64, Logical>,
    ) -> Option<(PointerFocusTarget, Point<f64, Logical>)> {
        // 1) Warstwy nakładkowe i górne (panele, menu systemowe) mają priorytet.
        if let Some(hit) = self.layer_surface_under(pos, &[WlrLayer::Overlay, WlrLayer::Top]) {
            return Some(hit);
        }

        // 2) Zwykłe okna.
        if let Some((window, window_loc)) = self.space.element_under(pos) {
            let window = window.clone();
            let window_loc = window_loc.to_f64();
            if let Some((surface, surface_loc)) =
                window.surface_under(pos - window_loc, WindowSurfaceType::ALL)
            {
                return Some((
                    PointerFocusTarget::WlSurface(surface),
                    surface_loc.to_f64() + window_loc,
                ));
            }
        }

        // 3) Warstwy dolne i tło (tapeta).
        self.layer_surface_under(pos, &[WlrLayer::Bottom, WlrLayer::Background])
    }

    /// Trafienie w warstwę danego typu (panel/tło) na którymś z wyjść.
    fn layer_surface_under(
        &self,
        pos: Point<f64, Logical>,
        wanted: &[WlrLayer],
    ) -> Option<(PointerFocusTarget, Point<f64, Logical>)> {
        for output in self.space.outputs() {
            let Some(output_geo) = self.space.output_geometry(output) else {
                continue;
            };
            if !output_geo.contains(pos.to_i32_round()) {
                continue;
            }

            let layers = layer_map_for_output(output);
            for kind in wanted {
                let Some(layer) = layers.layer_under(*kind, pos - output_geo.loc.to_f64()) else {
                    continue;
                };
                let layer_loc = layers
                    .layer_geometry(layer)
                    .map(|geometry| geometry.loc)
                    .unwrap_or_default();
                let local = pos - output_geo.loc.to_f64() - layer_loc.to_f64();
                if let Some((surface, surface_loc)) =
                    layer.surface_under(local, WindowSurfaceType::ALL)
                {
                    return Some((
                        PointerFocusTarget::WlSurface(surface),
                        surface_loc.to_f64() + layer_loc.to_f64() + output_geo.loc.to_f64(),
                    ));
                }
            }
        }
        None
    }

    /// Ustawia fokus klawiatury na oknie pod kursorem i podnosi je (Z-order).
    ///
    /// PO CO razem: w pływającym menedżerze kliknięcie w okno ma je zarówno
    /// aktywować, jak i wysunąć na wierzch. Robienie tego osobno prowadziłoby
    /// do rozjazdu (aktywne okno pod innym).
    pub fn update_keyboard_focus(&mut self, location: Point<f64, Logical>, serial: Serial) {
        // Gdy trwa grab (przeciąganie/zmiana rozmiaru) nie zmieniamy fokusu.
        if self.pointer.is_grabbed() {
            return;
        }
        if self
            .seat
            .get_keyboard()
            .map(|k| k.is_grabbed())
            .unwrap_or(false)
        {
            return;
        }

        let target = self
            .space
            .element_under(location)
            .map(|(window, _)| window.clone());
        if let Some(window) = target {
            self.raise_and_focus(&window, serial);
        }
    }

    /// Punkt wejścia dla zdarzeń z backendu okienkowego (winit/X11).
    pub fn process_input_event_windowed<B: InputBackend>(
        &mut self,
        event: InputEvent<B>,
        output_name: &str,
    ) {
        match event {
            InputEvent::Keyboard { event } => self.handle_keyboard::<B>(event),
            InputEvent::PointerMotionAbsolute { event } => {
                // Szukamy wyjścia PRZED wołaniem `&mut self`: gdyby wyszukanie i tak
                // zostało w wyrażeniu `if let`, iterator po `self.space` (pożyczka
                // niemutowalna) żyłby do końca bloku i kolidował z `&mut self`.
                let output = self
                    .space
                    .outputs()
                    .find(|o| o.name() == output_name)
                    .cloned();
                if let Some(output) = output {
                    self.on_pointer_move_absolute_windowed::<B>(event, &output);
                }
            }
            InputEvent::PointerButton { event } => self.on_pointer_button::<B>(event),
            InputEvent::PointerAxis { event } => self.on_pointer_axis::<B>(event),
            _ => {}
        }
    }

    /// Ruch myszy w trybie okienkowym (pozycja absolutna w obrębie okna backendu).
    fn on_pointer_move_absolute_windowed<B: InputBackend>(
        &mut self,
        evt: B::PointerMotionAbsoluteEvent,
        output: &Output,
    ) {
        let Some(output_geo) = self.space.output_geometry(output) else {
            return;
        };

        let pos = evt.position_transformed(output_geo.size) + output_geo.loc.to_f64();
        let serial = SCOUNTER.next_serial();

        self.pointer_location = pos;

        let pointer = self.pointer.clone();
        let under = self.surface_under(pos);
        pointer.motion(
            self,
            under,
            &MotionEvent {
                location: pos,
                serial,
                time: evt.time_msec(),
            },
        );
        pointer.frame(self);
    }

    /// Ruch myszy WZGLĘDNY — używany przez backend tty-udev (libinput podaje
    /// deltę, nie pozycję absolutną). Pozycję przeliczamy sami i przycinamy do
    /// obszaru wyjść, żeby kursor nie "uciekał" poza ekran.
    ///
    /// `allow(dead_code)`: bez feature `udev` ta funkcja nie ma wywołań, ale nie
    /// chcemy jej usuwać — to jedyna ścieżka ruchu względnego dla produkcji.
    #[allow(dead_code)]
    pub(crate) fn on_pointer_move<B: InputBackend>(&mut self, evt: B::PointerMotionEvent) {
        let new_location = self.clamp_to_outputs(self.pointer_location + evt.delta());
        let serial = SCOUNTER.next_serial();

        self.pointer_location = new_location;

        let pointer = self.pointer.clone();
        let under = self.surface_under(new_location);
        pointer.motion(
            self,
            under.clone(),
            &MotionEvent {
                location: new_location,
                serial,
                time: evt.time_msec(),
            },
        );

        // Osobno ruch względny — dla klientów korzystających z `wp_relative_pointer`
        // (np. gry). Delta bez akceleracji jest dokładniejsza dla sterowania.
        pointer.relative_motion(
            self,
            under,
            &RelativeMotionEvent {
                utime: evt.time(),
                delta: evt.delta(),
                delta_unaccel: evt.delta_unaccel(),
            },
        );
        pointer.frame(self);
    }

    /// Ogranicza punkt do obszaru któregoś z wyjść.
    ///
    /// PO CO: przy ruchu względnym łatwo wyjechać poza ekran. Przyciągamy punkt
    /// do NAJBLIŻSZEGO wyjścia (a nie do obwiedni), dzięki czemu przy układzie
    /// w kształcie litery L kursor nie "zapuszcza się" w martwe pole.
    fn clamp_to_outputs(&self, point: Point<f64, Logical>) -> Point<f64, Logical> {
        let areas: Vec<smithay::utils::Rectangle<i32, Logical>> = self
            .space
            .outputs()
            .filter_map(|output| self.space.output_geometry(output))
            .collect();
        if areas.is_empty() {
            return point;
        }

        let rounded = point.to_i32_round();
        if areas.iter().any(|area| area.contains(rounded)) {
            return point;
        }

        // Wybierz prostokąt o najmniejszym dystansie do punktu i przyciągnij do niego.
        let nearest = areas
            .iter()
            .min_by_key(|area| {
                let cx = rounded.x.clamp(area.loc.x, area.loc.x + area.size.w);
                let cy = rounded.y.clamp(area.loc.y, area.loc.y + area.size.h);
                let dx = rounded.x - cx;
                let dy = rounded.y - cy;
                dx * dx + dy * dy
            })
            .expect("areas nie jest puste");

        Point::from((
            point.x.clamp(
                nearest.loc.x as f64,
                (nearest.loc.x + nearest.size.w) as f64,
            ),
            point.y.clamp(
                nearest.loc.y as f64,
                (nearest.loc.y + nearest.size.h) as f64,
            ),
        ))
    }

    /// Klik myszy. Przy wciśnięciu najpierw aktualizujemy fokus/Z-order,
    /// a potem przekazujemy zdarzenie do klienta (żeby np. zadziałał przycisk).
    pub(crate) fn on_pointer_button<B: InputBackend>(&mut self, evt: B::PointerButtonEvent) {
        let serial = SCOUNTER.next_serial();
        let button = evt.button_code();
        let state = wl_pointer::ButtonState::from(evt.state());

        if wl_pointer::ButtonState::Pressed == state {
            // SSD: klik w pasek tytułu obsługujemy sami (przeciąganie / przyciski okna).
            // Konsumujemy zdarzenie, żeby nie poszło też do klienta.
            if self.handle_ssd_press(button, serial) {
                return;
            }
            self.update_keyboard_focus(self.pointer_location, serial);
        }

        let pointer = self.pointer.clone();
        pointer.button(
            self,
            &ButtonEvent {
                button,
                state: state.try_into().unwrap(),
                serial,
                time: evt.time_msec(),
            },
        );
        pointer.frame(self);
    }

    /// Najwyżej położone okno SSD, którego pasek tytułu zawiera punkt.
    ///
    /// PO CO `rev()`: `space.elements()` idzie od spodu do wierzchu, a my chcemy
    /// trafić w okno leżące na wierzchu.
    fn ssd_hit(&self, point: Point<f64, Logical>) -> Option<(PaganWindow, Point<i32, Logical>)> {
        for window in self.space.elements().rev() {
            let Some(location) = self.space.element_location(window) else {
                continue;
            };
            if window.titlebar_contains(location, point) {
                return Some((window.clone(), location));
            }
        }
        None
    }

    /// Najwyżej położone okno, którego ROZWINIĘTE menu zawiera punkt.
    fn ssd_menu_hit(
        &self,
        point: Point<f64, Logical>,
    ) -> Option<(PaganWindow, Point<i32, Logical>)> {
        for window in self.space.elements().rev() {
            let Some(location) = self.space.element_location(window) else {
                continue;
            };
            if window.ssd_menu_contains(location, point) {
                return Some((window.clone(), location));
            }
        }
        None
    }

    /// Czy jakiekolwiek okno ma otwarte menu.
    fn any_ssd_menu_open(&self) -> bool {
        self.space.elements().any(PaganWindow::ssd_menu_open)
    }

    /// Zamyka menu we wszystkich oknach.
    ///
    /// PO CO globalnie, a nie per okno: w danej chwili sensowne jest tylko jedno
    /// otwarte menu — inaczej kliknięcia „gubią się” między nakładającymi się panelami.
    fn close_all_ssd_menus(&self) {
        for window in self.space.elements() {
            if window.ssd_menu_open() {
                window.set_ssd_menu_open(false);
            }
        }
    }

    /// Obsługa wciśnięcia przycisku na pasku tytułu (SSD) oraz w menu okna.
    /// `true` = zdarzenie skonsumowane (nie idzie do klienta).
    fn handle_ssd_press(&mut self, button: u32, serial: Serial) -> bool {
        let point = self.pointer_location;

        // 1) Klik w rozwinięte menu okna — zależnie od rodzaju menu wykonaj akcję
        //    okna albo przekaż klik klientowi (menu aplikacji).
        if let Some((window, location)) = self.ssd_menu_hit(point) {
            self.raise_and_focus(&window, serial);
            let kind = window.ssd_menu_kind();
            let row = window.ssd_menu_row_at(location, point);
            let label_row = window.ssd_menu_label_row_at(location, point);
            window.set_ssd_menu_open(false);
            match kind {
                // Menu aplikacji: klik wiersza odsyłamy KLIENTOWI — tylko on zna
                // zawartość rozwinięcia i pokaże je pod właściwą etykietą.
                SsdMenuKind::AppMenu => {
                    if let Some(index) = label_row {
                        crate::app_menu::send_click(&window, location, index);
                    }
                }
                SsdMenuKind::Window => match row {
                    Some(TitlebarButton::Minimize) => self.minimize_window(&window),
                    Some(TitlebarButton::Maximize) => {
                        if window.is_maximized() {
                            self.unmaximize_window(&window);
                        } else {
                            self.maximize_window(&window);
                        }
                    }
                    Some(TitlebarButton::Close) => self.close_window(&window),
                    // Klik w tło panelu (poza wierszem) tylko zamyka menu.
                    _ => {}
                },
            }
            return true;
        }

        // 2) Klik w pasek tytułu.
        if let Some((window, location)) = self.ssd_hit(point) {
            self.raise_and_focus(&window, serial);
            match window.titlebar_button_at(location, point) {
                Some(TitlebarButton::Menu) => {
                    // Przełączamy: zapamiętujemy stan PRZED zamknięciem wszystkich,
                    // bo inaczej ponowny klik w burger nigdy by nie zamknął menu.
                    let was_open = window.ssd_menu_open();
                    self.close_all_ssd_menus();
                    window.set_ssd_menu_open(!was_open);
                }
                Some(TitlebarButton::Close) => {
                    self.close_all_ssd_menus();
                    self.close_window(&window);
                }
                Some(TitlebarButton::Minimize) => {
                    self.close_all_ssd_menus();
                    self.minimize_window(&window);
                }
                Some(TitlebarButton::Maximize) => {
                    self.close_all_ssd_menus();
                    if window.is_maximized() {
                        self.unmaximize_window(&window);
                    } else {
                        self.maximize_window(&window);
                    }
                }
                None => {
                    self.close_all_ssd_menus();
                    // Etykieta menu aplikacji (np. Plik/Edycja/Widok/Pomoc) leży w pasku,
                    // ale nie jest przyciskiem okna. Informujemy o kliknięciu KLIENTA —
                    // on zna zawartość swojego menu i pokaże rozwinięcie.
                    if let Some(index) = window.ssd_menu_label_at(location, point) {
                        crate::app_menu::send_click(&window, location, index);
                        return true;
                    }
                    // Przeciąganie za pasek — ten sam grab co przy `xdg_toplevel.move`.
                    let start_data = PointerGrabStartData {
                        focus: None,
                        button,
                        location: point,
                    };
                    let grab = PointerMoveSurfaceGrab {
                        start_data,
                        initial_window_location: location,
                        initial_window_size: window.geometry().size,
                        window,
                        current_snap: None,
                    };
                    self.snap_preview = None;
                    let pointer = self.pointer.clone();
                    pointer.set_grab(self, grab, serial, Focus::Clear);
                }
            }
            return true;
        }

        // 3) Klik poza paskiem i poza menu — zamknij ewentualne menu okna.
        //    Gdy menu było otwarte, konsumujemy klik, żeby nie trafił „przez”
        //    w treść okna leżącą pod menu.
        let menu_was_open = self.any_ssd_menu_open();
        self.close_all_ssd_menus();
        menu_was_open
    }

    /// Scroll. Przekazujemy do klienta pod kursorem (np. przewijanie treści).
    pub(crate) fn on_pointer_axis<B: InputBackend>(&mut self, evt: B::PointerAxisEvent) {
        let horizontal_amount = evt
            .amount(Axis::Horizontal)
            .unwrap_or_else(|| evt.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0);
        let vertical_amount = evt
            .amount(Axis::Vertical)
            .unwrap_or_else(|| evt.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0);
        let horizontal_amount_discrete = evt.amount_v120(Axis::Horizontal);
        let vertical_amount_discrete = evt.amount_v120(Axis::Vertical);

        let mut frame = AxisFrame::new(evt.time_msec()).source(evt.source());
        if horizontal_amount != 0.0 {
            frame = frame
                .relative_direction(Axis::Horizontal, evt.relative_direction(Axis::Horizontal));
            frame = frame.value(Axis::Horizontal, horizontal_amount);
            if let Some(discrete) = horizontal_amount_discrete {
                frame = frame.v120(Axis::Horizontal, discrete as i32);
            }
        }
        if vertical_amount != 0.0 {
            frame =
                frame.relative_direction(Axis::Vertical, evt.relative_direction(Axis::Vertical));
            frame = frame.value(Axis::Vertical, vertical_amount);
            if let Some(discrete) = vertical_amount_discrete {
                frame = frame.v120(Axis::Vertical, discrete as i32);
            }
        }
        if evt.source() == AxisSource::Finger {
            if evt.amount(Axis::Horizontal) == Some(0.0) {
                frame = frame.stop(Axis::Horizontal);
            }
            if evt.amount(Axis::Vertical) == Some(0.0) {
                frame = frame.stop(Axis::Vertical);
            }
        }

        let pointer = self.pointer.clone();
        pointer.axis(self, frame);
        pointer.frame(self);
    }
}

/// Skróty kompozytora (przechwytywane zanim trafią do klienta).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CompositorShortcut {
    /// Zakończ kompozytor.
    Quit,
    /// Przełącz fokus na następne okno.
    FocusNext,
    /// Przełącz fokus na poprzednie okno.
    FocusPrev,
    /// Zamknij okno z fokusem.
    CloseFocused,
    /// Schowaj okno z fokusem (minimalizacja).
    Minimize,
    /// Przywróć ostatnio schowane okno.
    Restore,
    /// Przeładuj motyw z pliku (gdy `inotify` nie zadziała albo dla pewności).
    ReloadTheme,
}

/// Rozpoznaje skrót globalny na podstawie modyfikatorów i klawisza.
///
/// PO CO: to jedyne miejsce, w którym kompozytor "konsumuje" klawiaturę zamiast
/// przekazać ją klientowi — użytkownik musi móc wyjść i przełączać okna.
fn compositor_shortcut(
    modifiers: &ModifiersState,
    handle: &KeysymHandle<'_>,
) -> Option<CompositorShortcut> {
    // Trzymamy się Ctrl+Alt, żeby nie kolidować ze skrótami aplikacji.
    if !(modifiers.ctrl && modifiers.alt) {
        return None;
    }

    match handle.modified_sym() {
        Keysym::BackSpace => Some(CompositorShortcut::Quit),
        Keysym::Left => Some(CompositorShortcut::FocusPrev),
        Keysym::Right => Some(CompositorShortcut::FocusNext),
        Keysym::Delete => Some(CompositorShortcut::CloseFocused),
        // CapsLock może zamienić literę na wielką — akceptujemy obie.
        Keysym::m | Keysym::M => Some(CompositorShortcut::Minimize),
        Keysym::u | Keysym::U => Some(CompositorShortcut::Restore),
        Keysym::r | Keysym::R => Some(CompositorShortcut::ReloadTheme),
        _ => None,
    }
}

impl MyCompositor {
    /// Obsługa klawiatury: najpierw skróty kompozytora, potem przekazanie do klienta.
    ///
    /// Ten sam kod obsługuje backend winit i udev.
    pub(crate) fn handle_keyboard<B: InputBackend>(&mut self, event: B::KeyboardKeyEvent) {
        let keyboard = self.seat.get_keyboard().unwrap();
        let serial = SCOUNTER.next_serial();
        let time = event.time_msec();
        let keycode = event.key_code();
        let state = event.state();

        let _ = keyboard.input(
            self,
            keycode,
            state,
            serial,
            time,
            |data, modifiers, handle| {
                if state == KeyState::Pressed {
                    if let Some(shortcut) = compositor_shortcut(modifiers, &handle) {
                        data.run_compositor_shortcut(shortcut);
                        return FilterResult::Intercept(true);
                    }
                }
                FilterResult::Forward::<bool>
            },
        );
    }

    /// Wykonuje rozpoznany skrót globalny.
    fn run_compositor_shortcut(&mut self, shortcut: CompositorShortcut) {
        match shortcut {
            CompositorShortcut::Quit => {
                tracing::info!("wyjście na życzenie (Ctrl+Alt+Backspace)");
                self.running.store(false, Ordering::SeqCst);
            }
            CompositorShortcut::FocusNext => self.focus_adjacent(1),
            CompositorShortcut::FocusPrev => self.focus_adjacent(-1),
            CompositorShortcut::CloseFocused => self.close_focused(),
            CompositorShortcut::Minimize => self.minimize_focused(),
            CompositorShortcut::Restore => self.restore_last_minimized(),
            CompositorShortcut::ReloadTheme => {
                // Motyw wczytuje się sam po zmianie pliku (inotify); ten skrót to
                // droga awaryjna i wygodne „odśwież na żądanie”.
                crate::theme::reload();
            }
        }
    }

    /// Przełącza fokus (i podnosi) na sąsiednie okno na liście przestrzeni.
    fn focus_adjacent(&mut self, direction: isize) {
        let windows: Vec<PaganWindow> = self.space.elements().cloned().collect();
        if windows.is_empty() {
            return;
        }

        let current = self
            .seat
            .get_keyboard()
            .and_then(|keyboard| keyboard.current_focus());
        let current_index = current.and_then(|target| match target {
            KeyboardFocusTarget::Window(window) => {
                windows.iter().position(|candidate| candidate.0 == window)
            }
            _ => None,
        });

        let next = current_index
            .map(|index| (index as isize + direction).rem_euclid(windows.len() as isize) as usize)
            .unwrap_or(0);

        let window = windows[next].clone();
        let serial = SCOUNTER.next_serial();
        self.raise_and_focus(&window, serial);
    }

    /// Prosi okno z fokusem o zamknięcie (protokół xdg-toplevel.close).
    fn close_focused(&mut self) {
        let current = self
            .seat
            .get_keyboard()
            .and_then(|keyboard| keyboard.current_focus());
        if let Some(KeyboardFocusTarget::Window(window)) = current {
            if let Some(toplevel) = window.toplevel() {
                toplevel.send_close();
            }
        }
    }

    /// Schowaj okno z fokusem (Ctrl+Alt+M).
    fn minimize_focused(&mut self) {
        let current = self
            .seat
            .get_keyboard()
            .and_then(|keyboard| keyboard.current_focus());
        if let Some(KeyboardFocusTarget::Window(window)) = current {
            // Fokus trzyma surowe `Window` — mapujemy je na `PaganWindow` z `space`.
            if let Some(window) = self.pagan_window_for(&window) {
                self.minimize_window(&window);
            }
        }
    }
}