🔒 Repository is read-only – file editing is disabled.
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);
}
}
}
}