//! 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, ) -> Option<(PointerFocusTarget, Point)> { // 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, wanted: &[WlrLayer], ) -> Option<(PointerFocusTarget, Point)> { 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, 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( &mut self, event: InputEvent, output_name: &str, ) { match event { InputEvent::Keyboard { event } => self.handle_keyboard::(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::(event, &output); } } InputEvent::PointerButton { event } => self.on_pointer_button::(event), InputEvent::PointerAxis { event } => self.on_pointer_axis::(event), _ => {} } } /// Ruch myszy w trybie okienkowym (pozycja absolutna w obrębie okna backendu). fn on_pointer_move_absolute_windowed( &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(&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) -> Point { let areas: Vec> = 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(&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) -> Option<(PaganWindow, Point)> { 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, ) -> Option<(PaganWindow, Point)> { 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(&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 { // 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(&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:: }, ); } /// 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 = 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); } } } }