//! Stan kompozytora i implementacje handlerów protokołów Waylanda. //! //! PO CO osobny `state.rs`: trzymamy tu cały "mózg" kompozytora — przestrzeń //! okien, seat (mysz/klawiatura), stany protokołów (xdg-shell, decoration, shm) //! oraz implementacje traitów Smithay. Logika renderowania jest celowo //! oddzielona (`render.rs`), a obsługa wejścia (`input.rs`), zgodnie z //! wymaganiem modularności. use std::{ cell::RefCell, os::unix::io::OwnedFd, sync::{atomic::AtomicBool, Arc, Mutex}, }; use tracing::{info, warn}; use smithay::{ backend::{ allocator::{dmabuf::Dmabuf, Format}, renderer::utils::on_commit_buffer_handler, }, delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_fractional_scale, delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_pointer_gestures, delegate_primary_selection, delegate_relative_pointer, delegate_seat, delegate_shm, delegate_text_input_manager, delegate_viewporter, delegate_xdg_activation, delegate_xdg_decoration, delegate_xdg_shell, desktop::{ find_popup_root_surface, layer_map_for_output, LayerSurface as DesktopLayerSurface, PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Space, Window, WindowSurfaceType, }, input::{ keyboard::XkbConfig, pointer::{CursorImageStatus, Focus, PointerHandle}, Seat, SeatHandler, SeatState, }, output::Output, reexports::{ calloop::{generic::Generic, Interest, LoopHandle, Mode, PostAction}, wayland_protocols::xdg::{ decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode, shell::server::xdg_toplevel, }, wayland_server::{ backend::{ClientData, ClientId, DisconnectReason}, protocol::{wl_data_source::WlDataSource, wl_output, wl_seat, wl_surface::WlSurface}, Client, Display, DisplayHandle, Resource, }, }, utils::{IsAlive, Logical, Point, Rectangle, Serial, Size, SERIAL_COUNTER as SCOUNTER}, wayland::{ buffer::BufferHandler, compositor::{ get_parent, with_states, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes, }, dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}, fractional_scale::{ with_fractional_scale, FractionalScaleHandler, FractionalScaleManagerState, }, output::{OutputHandler, OutputManagerState}, pointer_constraints::{ with_pointer_constraint, PointerConstraintsHandler, PointerConstraintsState, }, pointer_gestures::PointerGesturesState, relative_pointer::RelativePointerManagerState, seat::WaylandFocus, selection::{ data_device::{ set_data_device_focus, ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, }, primary_selection::{ set_primary_focus, PrimarySelectionHandler, PrimarySelectionState, }, SelectionHandler, }, shell::{ wlr_layer::{ Layer as WlrLayer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler, WlrLayerShellState, }, xdg::{ decoration::{XdgDecorationHandler, XdgDecorationState}, Configure, PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, XdgToplevelSurfaceData, }, }, shm::{ShmHandler, ShmState}, socket::ListeningSocketSource, text_input::TextInputManagerState, viewporter::ViewporterState, xdg_activation::{ XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, }, }, }; // `import_dmabuf` jest potrzebne tylko w backendzie udev (sprawdzamy import na GPU). #[cfg(feature = "udev")] use smithay::backend::renderer::ImportDma; // XWayland (aplikacje X11). use smithay::{ wayland::{ xwayland_keyboard_grab::XWaylandKeyboardGrabState, xwayland_shell::XWaylandShellState, }, xwayland::{X11Wm, XWaylandClientData}, }; use crate::{ focus::{KeyboardFocusTarget, PointerFocusTarget}, foreign_toplevel::ForeignToplevelState, grabs::{PointerMoveSurfaceGrab, PointerResizeSurfaceGrab, ResizeState, SurfaceData}, window::PaganWindow, }; /// Krok kaskadowego rozmieszczania nowych okien (w pikselach logicznych). /// PO CO: nowe okna mają się pojawiać "schodkowo", żeby nie nakładały się /// dokładnie na siebie i żeby było widać, że jest ich kilka. pub const CASCADE_STEP: i32 = 20; /// Dane klienta Waylanda. `CompositorClientState` jest wymagany przez Smithay /// do poprawnego działania protokołu `wl_surface` (m.in. buforów i fence'ów). #[derive(Debug, Default)] pub struct ClientState { pub compositor_state: CompositorClientState, } impl ClientData for ClientState { fn initialized(&self, _client_id: ClientId) {} fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} } /// Ikona przeciągania i upuszczania (drag & drop) trzymana na czas operacji. /// /// PO CO: DnD działa funkcjonalnie bez rysowania ikony, ale użytkownik powinien /// widzieć, co niesie. Trzymamy powierzchnię i przesunięcie; rysowanie podłączone /// jest w `render.rs` (`output_elements`). #[derive(Debug, Clone)] pub struct DndIcon { pub surface: WlSurface, pub offset: Point, } /// Okno odłożone na bok przez minimalizację. /// /// PO CO zapamiętujemy pozycję: bez menedżera zadań (paska zadań) okno musi /// wrócić dokładnie tam, gdzie było. Trzymamy więc okno ORAZ jego ostatnią /// pozycję w przestrzeni. #[derive(Debug, Clone)] pub struct MinimizedWindow { pub window: PaganWindow, pub location: Point, } /// Główny stan kompozytora. pub struct MyCompositor { /// Przestrzeń okien — kolejność elementów to Z-order (od spodu do wierzchu). pub space: Space, /// Pozycja kursora w przestrzeni logicznej (lustrzana do stanu pointera). pub pointer_location: Point, /// Nazwa socketa Waylanda (dla klientów: `WAYLAND_DISPLAY`). pub socket_name: Option, /// Flaga sterująca pętlą główną. pub running: Arc, /// Uchwyt do pętli zdarzeń — pozwala rejestrować źródła z wnętrza stanu. pub display_handle: DisplayHandle, pub handle: LoopHandle<'static, MyCompositor>, // Stany protokołów (globals). pub compositor_state: CompositorState, pub shm_state: ShmState, pub output_manager_state: OutputManagerState, pub seat_state: SeatState, pub xdg_shell_state: XdgShellState, pub xdg_decoration_state: XdgDecorationState, // Wejście. pub seat: Seat, pub pointer: PointerHandle, /// Menedżer popupów (menu rozwijane klientów). pub popups: PopupManager, /// Aktualny stan kursora (kształt/schowanie). pub cursor_status: CursorImageStatus, // --- Protokoły "biurkowe" (schowek, aktywacja, skala, ograniczenia) --- /// Schowek (`wl_data_device`) — bez tego kopiuj/wklej nie działa. pub data_device_state: DataDeviceState, /// Selekcja pierwotna (primary selection, wklejanie środkowym przyciskiem). pub primary_selection_state: PrimarySelectionState, /// Aktywacja okien (`xdg-activation`) — aplikacje mogą "poprosić o uwagę". pub xdg_activation_state: XdgActivationState, /// Skalowanie ułamkowe (`fractional-scale`) — podstawa HiDPI dla klientów. pub fractional_scale_manager_state: FractionalScaleManagerState, /// Ograniczenia wskaźnika (`pointer-constraints`) — np. gry przechwytujące kursor. pub pointer_constraints_state: PointerConstraintsState, /// Ikona przeciągania (DnD) na czas operacji. pub dnd_icon: Option, /// Zminimalizowane okna (unmapped z `space`) razem z ostatnią pozycją. /// /// PO CO poza `space`: `Space` rysuje wszystko, co zawiera — nie ma pojęcia /// "ukryte". Odkładamy więc okno tutaj i przywracamy na żądanie /// (skrót klawiszowy / przyszły pasek zadań). pub minimized: Vec, /// Stan protokołu `wlr-foreign-toplevel-management` (lista okien dla paneli). pub foreign_toplevel: ForeignToplevelState, /// Uchwyty globala `pagan_app_menu_manager_v1` (menu aplikacji w pasku SSD). /// /// PO CO trzymamy: obiekty menedżera żyją tak długo, jak ich klient — musimy /// je znać, żeby posprzątać po rozłączeniu i móc wysyłać zdarzenia. pub app_menu_managers: Vec, /// Podgląd („duch”) geometrii docelowej przy doklejaniu okna. /// /// PO CO: pokazujemy, gdzie okno wyląduje, ZANIM użytkownik puści przycisk — /// sam snap stosujemy dopiero przy puszczeniu (`grabs.rs`). pub snap_preview: Option>, // --- dmabuf + layer-shell --- /// Delegat protokołu `linux-dmabuf` (bufory GPU). pub dmabuf_state: DmabufState, /// Global dmabuf — trzymany, by nie został usunięty. pub dmabuf_global: Option, /// Stan warstw (`wlr-layer-shell`) — panele, tła, nakładki. pub layer_shell_state: WlrLayerShellState, /// Kolejka przechwyceń ekranu (`wlr-screencopy`) do wykonania w pętli renderu. pub screencopy_queue: Vec, // --- XWayland (aplikacje X11) --- /// Global protokołu `xwayland_shell` — bez niego XWayland nie ma jak /// skojarzyć okna X11 z powierzchnią Waylanda. pub xwayland_shell_state: XWaylandShellState, /// Menedżer okien X11 (reparenting WM). `None`, dopóki XWayland nie zgłosi /// gotowości (albo gdy binarki `Xwayland` nie ma w systemie). pub xwm: Option, /// Numer wyświetlacza X (dla `DISPLAY=:N` uruchamianych aplikacji X11). pub xdisplay: Option, /// Konfiguracja doklejania okien (próg + dolna krawędź), z `PAGAN_SNAP_*`. pub snap_config: crate::snap::SnapConfig, /// Stan backendu tty-udev (DRM/KMS + libinput). `None` dla backendu winit. #[cfg(feature = "udev")] pub udev: Option, /// Następna pozycja dla kaskadowego rozmieszczania (współdzielona z `output.rs`). pub(crate) next_cascade: Point, } impl MyCompositor { /// Tworzy stan kompozytora, rejestruje socket Waylanda, źródło dispatchu /// klientów oraz wszystkie globalne protokoły. /// /// PO CO tutaj, a nie w `run_winit`: to jest logika niezależna od backendu — /// ten sam `init` używają oba backendy (winit i tty-udev). Wyjściami zajmuje /// się `output.rs`/backend, bo dopiero one znają tryby i skalę. pub fn init( display: Display, handle: LoopHandle<'static, Self>, listen_on_socket: bool, ) -> Self { let dh = display.handle(); // Socket, na którym klienci łączą się z kompozytorem. let socket_name = if listen_on_socket { let source = ListeningSocketSource::new_auto().expect("nie udało się utworzyć socketa Wayland"); let name = source.socket_name().to_string_lossy().into_owned(); handle .insert_source(source, |stream, _, data: &mut Self| { if let Err(err) = data .display_handle .insert_client(stream, Arc::new(ClientState::default())) { warn!("Błąd dodawania klienta Waylanda: {}", err); } }) .expect("nie udało się zarejestrować źródła socketa"); info!(name = name, "PaganDE: nasłuchuję na sockecie Wayland"); Some(name) } else { None }; // Źródło calloop czytające zdarzenia przychodzące od klientów. handle .insert_source( Generic::new(display, Interest::READ, Mode::Level), |_, display, data: &mut Self| { // Bezpieczeństwo: `Display` żyje tak długo jak źródło calloop, // więc wskaźnik jest ważny przez cały czas działania pętli. unsafe { display .get_mut() .dispatch_clients(data) .expect("dispatch_clients nie powiodło się"); } Ok(PostAction::Continue) }, ) .expect("nie udało się zarejestrować źródła display"); // Globalne protokoły. let compositor_state = CompositorState::new::(&dh); let shm_state = ShmState::new::(&dh, vec![]); let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); let xdg_shell_state = XdgShellState::new::(&dh); let xdg_decoration_state = XdgDecorationState::new::(&dh); // Protokoły "biurkowe": schowek, primary selection, aktywacja, skala, // ograniczenia wskaźnika oraz text-input (global dla kompatybilności). let data_device_state = DataDeviceState::new::(&dh); let primary_selection_state = PrimarySelectionState::new::(&dh); let xdg_activation_state = XdgActivationState::new::(&dh); let fractional_scale_manager_state = FractionalScaleManagerState::new::(&dh); let pointer_constraints_state = PointerConstraintsState::new::(&dh); // Text-input tworzymy tylko jako global (nie implementujemy jeszcze // input-method), więc nie potrzebujemy przechowywać jego stanu. TextInputManagerState::new::(&dh); // Warstwy (panele/menus), viewporter oraz wskaźnik względny i gesty. let layer_shell_state = WlrLayerShellState::new::(&dh); ViewporterState::new::(&dh); RelativePointerManagerState::new::(&dh); PointerGesturesState::new::(&dh); // Screencopy: rejestrujemy global; logika przechwycenia w `screencopy.rs`. crate::screencopy::init(&dh); // Lista i sterowanie oknami dla paneli/pasków zadań. crate::foreign_toplevel::init(&dh); // Menu aplikacji w pasku SSD (własny protokół PaganDE). crate::app_menu::init(&dh); // XWayland: `xwayland_shell` (skojarzenie okna X11 z wl_surface) oraz grab // klawiatury X11. `XWaylandKeyboardGrabState` nie wymaga pola — trzyma go // display; tworzymy wyłącznie po to, by zarejestrować global. let xwayland_shell_state = XWaylandShellState::new::(&dh); XWaylandKeyboardGrabState::new::(&dh); // Seat: mysz + klawiatura. Klawiaturę tworzymy z domyślną mapą xkb. let mut seat_state = SeatState::new(); let mut seat = seat_state.new_wl_seat(&dh, "pagan".to_string()); let pointer = seat.add_pointer(); seat.add_keyboard(XkbConfig::default(), 200, 25) .expect("nie udało się zainicjalizować klawiatury"); // Kaskadowe rozmieszczanie startuje w punkcie (CASCADE_STEP, CASCADE_STEP). // PO CO nie liczymy tego z trybu wyjścia: tryb jest w jednostkach // FIZYCZNYCH, a `next_cascade` jest logiczny. Nowe okna i tak trafiają // na monitor pod kursorem przy mapowaniu (`map_window`). let next_cascade = Point::from((CASCADE_STEP, CASCADE_STEP)); // Motyw kompozytora: wczytaj plik (utwórz szablon przy pierwszym starcie) // i obserwuj zmiany. Wspólne dla obu backendów, bo `init` jest wspólny. crate::theme::install(&handle); MyCompositor { space: Space::default(), pointer_location: Point::from((0.0, 0.0)), socket_name, running: Arc::new(AtomicBool::new(true)), display_handle: dh, handle, compositor_state, shm_state, output_manager_state, seat_state, xdg_shell_state, xdg_decoration_state, seat, pointer, popups: PopupManager::default(), cursor_status: CursorImageStatus::default_named(), data_device_state, primary_selection_state, xdg_activation_state, fractional_scale_manager_state, pointer_constraints_state, dnd_icon: None, minimized: Vec::new(), foreign_toplevel: ForeignToplevelState::new(), app_menu_managers: Vec::new(), snap_preview: None, dmabuf_state: DmabufState::new(), dmabuf_global: None, layer_shell_state, screencopy_queue: Vec::new(), xwayland_shell_state, xwm: None, xdisplay: None, snap_config: crate::snap::SnapConfig::from_env(), #[cfg(feature = "udev")] udev: None, next_cascade, } } /// Dołącza stan backendu tty-udev po jego zbudowaniu. /// /// PO CO osobno: `UdevState` potrzebuje `LoopHandle`, który powstaje dopiero /// tutaj, a jednocześnie musi zostać zarejestrowany w źródłach calloop. #[cfg(feature = "udev")] pub fn attach_udev(&mut self, udev: crate::udev::UdevState) { self.udev = Some(udev); } /// Rejestruje global `linux-dmabuf` z podanymi formatami. /// /// PO CO osobno od `init`: formaty pochodzą z renderera, a renderer powstaje /// dopiero po zbudowaniu stanu (w backencie). Metoda jest więc wspólna dla /// winit i udev i wołana raz, tuż po starcie backendu. pub fn enable_dmabuf(&mut self, formats: impl IntoIterator) { if self.dmabuf_global.is_some() { return; } let global = self .dmabuf_state .create_global::(&self.display_handle, formats); self.dmabuf_global = Some(global); info!("global linux-dmabuf zarejestrowany"); } /// Znajduje okno na podstawie powierzchni Waylanda. pub fn window_for_surface(&self, surface: &WlSurface) -> Option { self.space .elements() .find(|window| window.wl_surface().map(|s| &*s == surface).unwrap_or(false)) .cloned() } /// Mapuje nowe okno w układzie kaskadowym, na wyjściu pod kursorem. /// /// Wariant publicznego API z Zadania 1 (`map_window`). Właściwa logika /// (wybór monitora + kaskada) żyje w `crate::output`. pub fn map_window(&mut self, window: PaganWindow) { // Kolejność: najpierw umieszczenie w przestrzeni (kaskada), potem fokus. self.place_new_window(window.clone()); // Nowo otwarte okno OD RAZU przejmuje fokus klawiatury. Dzięki temu jest // od razu wizualnie aktywne (jaśniejszy pasek) i przyjmuje klawisze — // bez tego użytkownik musiałby najpierw kliknąć w nowe okno. let serial = SCOUNTER.next_serial(); self.raise_and_focus(&window, serial); } /// Przesuwa okno o wektor (dx, dy) w pikselach logicznych. pub fn move_window(&mut self, window: &PaganWindow, dx: f64, dy: f64) { if let Some(loc) = self.space.element_location(window) { let new_loc = loc + Point::from((dx.round() as i32, dy.round() as i32)); self.space.map_element(window.clone(), new_loc, false); } } /// Minimalizuje okno: chowa je z przestrzeni i odkłada na listę schowanych. /// /// PO CO `unmap_elem`, a nie przesunięcie za ekran: okno ma zniknąć zarówno /// z kompozycji, jak i z trafień myszy/fokusa. Okno poza `space` nie jest /// rysowane ani nie łapie kliknięć. pub fn minimize_window(&mut self, window: &PaganWindow) { // Powtórna minimalizacja nie może zdublować wpisu na liście. if self.minimized.iter().any(|entry| &entry.window == window) { return; } let Some(location) = self.space.element_location(window) else { // Okno nie jest zmapowane — nie ma czego chować. return; }; self.space.unmap_elem(window); self.minimized.push(MinimizedWindow { window: window.clone(), location, }); // Fokus nie może wskazywać na okno, którego nie ma już w przestrzeni. let serial = SCOUNTER.next_serial(); self.focus_top_window(serial); } /// Przywraca ostatnio zminimalizowane okno (stos LIFO). /// /// PO CO LIFO: to naturalna kolejność dla użytkownika — ostatnio schowane /// wraca pierwsze, jak zdejmowanie ze stosu kartek. pub fn restore_last_minimized(&mut self) { let serial = SCOUNTER.next_serial(); // Pomijamy okna, które klient zdążył zamknąć, gdy były schowane. while let Some(entry) = self.minimized.pop() { if !entry.window.alive() { continue; } self.space .map_element(entry.window.clone(), entry.location, true); self.raise_and_focus(&entry.window, serial); return; } tracing::info!("brak zminimalizowanych okien do przywrócenia"); } /// Nadaje fokus oknu z wierzchu stosu (albo czyści fokus, gdy nic nie zostało). fn focus_top_window(&mut self, serial: Serial) { // `elements()` idzie od spodu do wierzchu, więc ostatni element = wierzch. // Kolekcjonujemy od razu do `Option`, żeby nie trzymać pożyczki `space` // podczas mutującego `raise_and_focus`. let top = self.space.elements().next_back().cloned(); match top { Some(window) => self.raise_and_focus(&window, serial), None => { if let Some(keyboard) = self.seat.get_keyboard() { keyboard.set_focus(self, Option::::None, serial); } } } } /// Zamienia surowe okno Smithaya na nasze okno z dekoracją SSD (`PaganWindow`). /// /// PO CO: fokus klawiatury trzyma `Window` (tak wymaga Smithay), a operacje /// typu minimalizacja potrzebują wrappera `PaganWindow` obecnego w `space`. pub fn pagan_window_for(&self, window: &Window) -> Option { self.space .elements() .find(|candidate| &candidate.0 == window) .cloned() } /// Przywraca KONKRETNE okno z listy zminimalizowanych. /// /// PO CO osobno od `restore_last_minimized`: panel wie, który kafelek /// kliknięto, więc przywracamy wskazane okno, a nie ostatnie z brzegu. pub fn restore_window(&mut self, window: &PaganWindow) { let Some(position) = self .minimized .iter() .position(|entry| &entry.window == window) else { return; }; let entry = self.minimized.remove(position); self.space .map_element(entry.window.clone(), entry.location, true); let serial = SCOUNTER.next_serial(); self.raise_and_focus(&entry.window, serial); } /// Podnosi i aktywuje okno; jeśli było schowane — najpierw je przywraca. /// /// To wspólna akcja dla skrótu, kliknięcia w pasek zadań i protokołu /// `foreign-toplevel`. pub fn focus_window(&mut self, window: &PaganWindow) { if self.minimized.iter().any(|entry| &entry.window == window) { self.restore_window(window); return; } if !window.alive() { return; } let serial = SCOUNTER.next_serial(); self.raise_and_focus(window, serial); } /// Prosi okno o zamknięcie (Wayland: `xdg_toplevel.close`, X11: `WM_DELETE_WINDOW`). pub fn close_window(&mut self, window: &PaganWindow) { if let Some(toplevel) = window.toplevel() { toplevel.send_close(); } else if let Some(x11) = window.x11_surface() { let _ = x11.close(); } } /// Maksymalizuje okno (w pływającym WM = obszar roboczy wyjścia pod oknem). pub fn maximize_window(&mut self, window: &PaganWindow) { if let Some(toplevel) = window.toplevel() { let Some(area) = self.output_work_area_for_window(window) else { return; }; toplevel.with_pending_state(|state| { state.states.set(xdg_toplevel::State::Maximized); state.size = Some(area.size); }); self.space.map_element(window.clone(), area.loc, true); toplevel.send_configure(); } else if let Some(x11) = window.x11_surface() { // Dla X11 zmiana stanu jest prośbą do klienta; geometrią zajmuje się // `XwmHandler::maximize_request` (zapamiętuje starą geometrię). let _ = x11.set_maximized(true); } } /// Cofa maksymalizację. pub fn unmaximize_window(&mut self, window: &PaganWindow) { if let Some(toplevel) = window.toplevel() { toplevel.with_pending_state(|state| { state.states.unset(xdg_toplevel::State::Maximized); state.size = None; }); toplevel.send_pending_configure(); } else if let Some(x11) = window.x11_surface() { let _ = x11.set_maximized(false); } } /// Pełny ekran na wyjściu pod oknem. pub fn set_fullscreen_window(&mut self, window: &PaganWindow) { if let Some(toplevel) = window.toplevel() { let Some(area) = self.output_area_for_window(window) else { return; }; toplevel.with_pending_state(|state| { state.states.set(xdg_toplevel::State::Fullscreen); state.size = Some(area.size); }); self.space.map_element(window.clone(), area.loc, true); toplevel.send_configure(); } else if let Some(x11) = window.x11_surface() { let _ = x11.set_fullscreen(true); } } /// Wychodzi z pełnego ekranu. pub fn unset_fullscreen_window(&mut self, window: &PaganWindow) { if let Some(toplevel) = window.toplevel() { toplevel.with_pending_state(|state| { state.states.unset(xdg_toplevel::State::Fullscreen); state.size = None; }); toplevel.send_pending_configure(); } else if let Some(x11) = window.x11_surface() { let _ = x11.set_fullscreen(false); } } /// Prostokąt wyjścia, na którym leży okno (a jeśli nie wiadomo — pierwszego). fn output_area_for_window(&self, window: &PaganWindow) -> Option> { let location = self.space.element_location(window); let under = location.and_then(|location| { self.space.outputs().find_map(|output| { let geometry = self.space.output_geometry(output)?; geometry.contains(location).then_some(geometry) }) }); under.or_else(|| { self.space .outputs() .next() .and_then(|output| self.space.output_geometry(output)) }) } /// Prostokąt OBSZARU ROBOCZEGO wyjścia, na którym leży okno (bez paneli). /// /// PO CO osobno od `output_area_for_window`: pełny ekran ma zasłaniać panel, /// a maksymalizacja — nie (okno zmaksymalizowane nadal widzi pasek systemowy). fn output_work_area_for_window(&self, window: &PaganWindow) -> Option> { let location = self.space.element_location(window); let under = location.and_then(|location| { self.space.outputs().find_map(|output| { let geometry = self.space.output_geometry(output)?; geometry .contains(location) .then(|| self.output_work_area(output)) .flatten() }) }); under.or_else(|| { self.space .outputs() .next() .and_then(|output| self.output_work_area(output)) }) } /// Prosi klienta o zmianę rozmiaru (poprzez nowy configure xdg-toplevel). /// /// PO CO przez configure, a nie bezpośrednio: w hybrydowym CSD to klient /// renderuje okno i ma ostatnie słowo co do rozmiaru bufora. My jedynie /// sugerujemy rozmiar, a klient potwierdza go commitem. pub fn resize_window(&mut self, window: &PaganWindow, new_size: Size) { if let Some(toplevel) = window.toplevel() { toplevel.with_pending_state(|state| { state.size = Some(new_size); }); toplevel.send_configure(); } } /// Podnosi okno na wierzch stosu (Z-order) i nadaje mu fokus klawiatury. /// /// PO CO w jednym miejscu: to jedna operacja semantyczna — "użytkownik /// kliknął okno". Wywołujemy ją przy każdym kliknięciu w powierzchnię. pub fn raise_and_focus(&mut self, window: &PaganWindow, serial: Serial) { self.space.raise_element(window, true); let keyboard = self.seat.get_keyboard().unwrap(); keyboard.set_focus( self, Some(KeyboardFocusTarget::Window(window.0.clone())), serial, ); } /// Wysyła początkowy configure do nowo zmapowanego toplevela (jeśli trzeba) /// i domyka stan resize po commicie klienta. fn ensure_initial_configure(&mut self, surface: &WlSurface) { let Some(window) = self.window_for_surface(surface) else { return; }; if let Some(toplevel) = window.toplevel() { let initial_configure_sent = with_states(surface, |states| { states .data_map .get::() .map(|data| data.lock().unwrap().initial_configure_sent) .unwrap_or(false) }); if !initial_configure_sent { toplevel.send_configure(); } } // Gdy klient zacommitował finalny rozmiar po resize, czyścimy stan. with_states(surface, |states| { if let Some(data) = states.data_map.get::>() { let mut data = data.borrow_mut(); if let ResizeState::WaitingForCommit(_) = data.resize_state { data.resize_state = ResizeState::NotResizing; } } }); } /// Obsługuje żądanie przeciągnięcia okna (`xdg_toplevel.move`). /// /// Uwaga: to jest standardowa ścieżka dla hybrydowego CSD — klient wysyła /// `move` z serialem kliknięcia, a my ustanawiamy grab. pub fn move_request_xdg( &mut self, surface: &ToplevelSurface, seat: &Seat, serial: Serial, ) { let Some(pointer) = seat.get_pointer() else { return; }; if !pointer.has_grab(serial) { return; } let Some(start_data) = pointer.grab_start_data() else { return; }; let Some(window) = self.window_for_surface(surface.wl_surface()) else { return; }; // Ignorujemy żądania z innej powierzchni niż ta, która ma fokus. let same_client = start_data .focus .as_ref() .map(|(focus, _)| focus.same_client_as(&surface.wl_surface().id())) .unwrap_or(false); if !same_client { return; } // --- Doklejanie: pamiętamy „swobodny” rozmiar okna --- // // PO CO: gdy okno jest doklejone (kafel/ćwiartka) albo zmaksymalizowane, // chcemy, żeby po ponownym chwyceniu "wyskoczyło" do rozmiaru SPRZED // pierwszego doklejenia — a nie ciągnęło się w rozmiarze ekranu za kursorem. // Trzymamy go w `user_data` okna, więc przeżywa kolejne graby. let current_states = surface.current_state().states; // `ToplevelStateSet` nie ma `intersects` — sprawdzamy każdy stan osobno. let is_tiled = current_states.contains(xdg_toplevel::State::Maximized) || current_states.contains(xdg_toplevel::State::TiledLeft) || current_states.contains(xdg_toplevel::State::TiledRight) || current_states.contains(xdg_toplevel::State::TiledTop) || current_states.contains(xdg_toplevel::State::TiledBottom); let free_size_cell = window .user_data() .get_or_insert(|| Mutex::new(window.geometry().size)); let free_size = *free_size_cell.lock().expect("rozmiar swobodny zatruty"); let (initial_window_size, initial_window_location) = if is_tiled { // Wyskakujemy z kafla/maksymalizacji od razu ustawiając swobodny rozmiar. surface.with_pending_state(|state| { state.states.unset(xdg_toplevel::State::TiledLeft); state.states.unset(xdg_toplevel::State::TiledRight); state.states.unset(xdg_toplevel::State::TiledTop); state.states.unset(xdg_toplevel::State::TiledBottom); state.states.unset(xdg_toplevel::State::Maximized); state.size = Some(free_size); }); surface.send_pending_configure(); let pos = pointer.current_location(); (free_size, Point::from((pos.x as i32, pos.y as i32))) } else { // Okno swobodne — zapamiętaj jego rozmiar jako punkt powrotu po snappie. let size = window.geometry().size; *free_size_cell.lock().expect("rozmiar swobodny zatruty") = size; let location = self .space .element_location(&window) .unwrap_or_else(|| Point::from((0, 0))); (size, location) }; let grab = PointerMoveSurfaceGrab { start_data, window, initial_window_location, initial_window_size, current_snap: None, }; pointer.set_grab(self, grab, serial, Focus::Clear); } /// Obsługuje żądanie zmiany rozmiaru okna (`xdg_toplevel.resize`). pub fn resize_request_xdg( &mut self, surface: &ToplevelSurface, seat: &Seat, serial: Serial, edges: xdg_toplevel::ResizeEdge, ) { let Some(pointer) = seat.get_pointer() else { return; }; if !pointer.has_grab(serial) { return; } let Some(start_data) = pointer.grab_start_data() else { return; }; let Some(window) = self.window_for_surface(surface.wl_surface()) else { return; }; let same_client = start_data .focus .as_ref() .map(|(focus, _)| focus.same_client_as(&surface.wl_surface().id())) .unwrap_or(false); if !same_client { return; } let geometry = window.geometry(); let location = self .space .element_location(&window) .unwrap_or_else(|| Point::from((0, 0))); let (initial_window_location, initial_window_size) = (location, geometry.size); with_states(surface.wl_surface(), move |states| { states .data_map .get::>() .expect("SurfaceData brak — patrz new_toplevel") .borrow_mut() .resize_state = ResizeState::Resizing(crate::grabs::ResizeData { edges: edges.into(), initial_window_location, initial_window_size, }); }); let grab = PointerResizeSurfaceGrab { start_data, window, edges: edges.into(), initial_window_location, initial_window_size, last_window_size: initial_window_size, }; pointer.set_grab(self, grab, serial, Focus::Clear); } } impl CompositorHandler for MyCompositor { fn compositor_state(&mut self) -> &mut CompositorState { &mut self.compositor_state } fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { // Klient XWayland ma WŁASNY typ danych (tworzy go Smithay przy `XWayland::spawn`) // i też potrzebuje stanu kompozytora — bez tego panikowalibyśmy przy pierwszym // bindowaniu `wl_output` przez XWayland (a to psuje całą sesję). if let Some(state) = client.get_data::() { return &state.compositor_state; } if let Some(state) = client.get_data::() { return &state.compositor_state; } panic!("nieznany typ danych klienta — patrz init()") } fn new_surface(&mut self, _surface: &WlSurface) { // Nic nie robimy: w tym szkieletie nie importujemy dmabufów ani nie // montujemy pre-commit hooków. SHM obsługiwany jest przez ShmState. } fn commit(&mut self, surface: &WlSurface) { // Przetwarza bufor powierzchni (wymagane przez Smithay). on_commit_buffer_handler::(surface); // Znajdujemy korzeń drzewa powierzchni (dla ewentualnych subsurface'ów). let mut root = surface.clone(); while let Some(parent) = get_parent(&root) { root = parent; } if let Some(window) = self.window_for_surface(&root) { window.on_commit(); if &root == surface { // Klient może poprosić o przesunięcie okna (buffer_delta) — // np. przy synchronizacji zawartości. Respektujemy to. let buffer_offset = with_states(surface, |states| { states .cached_state .get::() .current() .buffer_delta .take() }); if let Some(offset) = buffer_offset { if let Some(location) = self.space.element_location(&window) { self.space .map_element(window.clone(), location + offset, false); } } self.ensure_initial_configure(surface); } } // Jeśli commituje powierzchnia ikony przeciągania (DnD), aktualizujemy jej // przesunięcie. Klient może przesuwać ikonę względem kursora (`buffer_delta`), // np. gdy przeciąga się kolumnę tabeli — chcemy to odwzorować. if let Some(icon) = self.dnd_icon.as_mut() { if &icon.surface == surface { let buffer_delta = with_states(surface, |states| { states .cached_state .get::() .current() .buffer_delta .take() }) .unwrap_or_default(); icon.offset += buffer_delta; } } // Warstwy (panele/paski): po commicie klienta przeliczamy strefy i — jeśli // to pierwszy commit — wysyłamy PIERWSZY `configure`. Smithay celowo nie // robi tego w `arrange()` (patrz komentarz w `layer.rs`), więc musi to // zrobić kompozytor. Bez tego panel nie wie, jak szeroki ma być. if let Some(output) = self .space .outputs() .find(|output| { layer_map_for_output(output) .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) .is_some() }) .cloned() { let initial_configure_sent = with_states(surface, |states| { states .data_map .get::() .map(|data| data.lock().unwrap().initial_configure_sent) .unwrap_or(false) }); let mut map = layer_map_for_output(&output); // Najpierw przelicz (uwzględnij rozmiar/anchory podane przez klienta), // potem wyślij pierwszy configure. map.arrange(); if !initial_configure_sent { if let Some(layer) = map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) { layer.layer_surface().send_configure(); } } } self.popups.commit(surface); } } impl ShmHandler for MyCompositor { fn shm_state(&self) -> &ShmState { &self.shm_state } } impl OutputHandler for MyCompositor {} impl BufferHandler for MyCompositor { fn buffer_destroyed( &mut self, _buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer, ) { } } impl SeatHandler for MyCompositor { type KeyboardFocus = KeyboardFocusTarget; type PointerFocus = PointerFocusTarget; type TouchFocus = PointerFocusTarget; fn seat_state(&mut self) -> &mut SeatState { &mut self.seat_state } /// Gdy zmienia się fokus klawiatury, przekazujemy to schowkowi i selekcji /// pierwotnej — bez tego kopiuj/wklej "nie wie", który klient jest aktywny. fn focus_changed(&mut self, seat: &Seat, target: Option<&KeyboardFocusTarget>) { let dh = &self.display_handle; let wl_surface = target .and_then(WaylandFocus::wl_surface) .map(|surface| surface.into_owned()); let client = wl_surface .as_ref() .and_then(|surface| dh.get_client(surface.id()).ok()); set_data_device_focus(dh, seat, client.clone()); set_primary_focus(dh, seat, client); // Wyróżnij wizualnie okno AKTYWNE (firmowy pasek aktywny + akcent), // a pozostałe przygaś. Porównujemy po OKNIE, a nie po `wl_surface`: // okno X11 (XWayland) może nie mieć jeszcze powiązanej powierzchni // Waylanda w chwili nadawania fokusu, a mimo to jest aktywnym oknem. for window in self.space.elements() { let is_focused = matches!( target, Some(KeyboardFocusTarget::Window(focused)) if focused == &window.0 ); window.set_focused(is_focused); } } fn cursor_image(&mut self, _seat: &Seat, image: CursorImageStatus) { self.cursor_status = image; } } impl XdgDecorationHandler for MyCompositor { fn new_decoration(&mut self, toplevel: ToplevelSurface) { // DECYZJA: wymuszamy tryb SERVER-SIDE. Od tego momentu to KOMPOZYTOR // rysuje pasek tytułu — spójnie dla aplikacji, które znają ten protokół // (nasz toolkit, Qt, GTK4). // // Klienci, którzy NIE tworzą tego obiektu (np. GTK3), rysują własne CSD — // dla nich `PaganWindow::wants_ssd` zwróci `false`, żeby nie nakładać // dwóch pasków (patrz `mark_ssd_negotiated`). toplevel.with_pending_state(|state| { state.decoration_mode = Some(DecorationMode::ServerSide); }); crate::window::mark_ssd_negotiated(&toplevel); // Negocjacja potrafi przyjść PO pierwszym umieszczeniu okna w kaskadzie // (klient tworzy obiekt dekoracji chwilę po pierwszym commicie). Wtedy // okno stoi już tak, jakby paska nie było — dosuwamy je, żeby nasz pasek // nie schował się pod panelem. self.reposition_for_ssd(&toplevel); tracing::debug!("dekoracja: klient negocjuje (new_decoration)"); } fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) { // Ignorujemy preferencję klienta — dekoracje rysujemy my. toplevel.with_pending_state(|state| { state.decoration_mode = Some(DecorationMode::ServerSide); }); tracing::debug!("dekoracja: request_mode -> ServerSide"); } fn unset_mode(&mut self, toplevel: ToplevelSurface) { // Brak preferencji klienta = nadal nasz pasek serwerowy. toplevel.with_pending_state(|state| { state.decoration_mode = Some(DecorationMode::ServerSide); }); } } impl MyCompositor { /// Dosuwa już umieszczone okno tak, aby pasek SSD zmieścił się na ekranie. /// /// PO CO osobno od `place_new_window`: w momencie pierwszego mapowania okno /// Waylanda może jeszcze nie mieć wynegocjowanych dekoracji, więc ląduje /// wyżej. Gdy klient je zgłosi, korygujemy pozycję (bez zmiany rozmiaru). fn reposition_for_ssd(&mut self, toplevel: &ToplevelSurface) { let Some(window) = self.window_for_surface(toplevel.wl_surface()) else { return; }; let Some(location) = self.space.element_location(&window) else { return; }; let adjusted = self.adjust_for_ssd(&window, location); if adjusted != location { self.space.map_element(window, adjusted, false); } } } impl XdgShellHandler for MyCompositor { fn xdg_shell_state(&mut self) -> &mut XdgShellState { &mut self.xdg_shell_state } fn new_toplevel(&mut self, surface: ToplevelSurface) { // Nie wysyłamy configure tutaj — Smithay wymaga, by początkowy // configure poszedł przy pierwszym commicie (patrz `ensure_initial_configure`). let window = PaganWindow::new(surface.clone()); // Doklejamy `SurfaceData` do powierzchni — używa jej grab resize do // śledzenia stanu operacji. with_states(surface.wl_surface(), |states| { states .data_map .insert_if_missing(|| RefCell::new(SurfaceData::default())); }); self.map_window(window); } fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { if let Err(err) = self.popups.track_popup(PopupKind::Xdg(surface)) { warn!("Nie udało się śledzić popupu: {}", err); } } fn reposition_request( &mut self, surface: PopupSurface, positioner: PositionerState, token: u32, ) { surface.with_pending_state(|state| { state.geometry = positioner.get_geometry(); state.positioner = positioner; }); surface.send_repositioned(token); } fn move_request(&mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial) { if let Some(seat) = Seat::from_resource(&seat) { self.move_request_xdg(&surface, &seat, serial); } } fn resize_request( &mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial, edges: xdg_toplevel::ResizeEdge, ) { if let Some(seat) = Seat::from_resource(&seat) { self.resize_request_xdg(&surface, &seat, serial, edges); } } fn ack_configure(&mut self, surface: WlSurface, configure: Configure) { // Dokańczamy maszynę stanów resize, gdy klient potwierdził finalny configure. // `Configure` to enum; serial niesie wariant toplevela (popupy go nie potrzebują). if let Configure::Toplevel(configure) = configure { let waiting_serial = with_states(&surface, |states| { states .data_map .get::>() .and_then(|data| { if let ResizeState::WaitingForFinalAck(_, serial) = data.borrow().resize_state { Some(serial) } else { None } }) }); if let Some(serial) = waiting_serial { let is_resizing = with_states(&surface, |states| { states .data_map .get::() .map(|data| { data.lock() .unwrap() .current .states .contains(xdg_toplevel::State::Resizing) }) .unwrap_or(false) }); if configure.serial >= serial && is_resizing { with_states(&surface, |states| { if let Some(data) = states.data_map.get::>() { let mut data = data.borrow_mut(); if let ResizeState::WaitingForFinalAck(resize_data, _) = data.resize_state { data.resize_state = ResizeState::WaitingForCommit(resize_data); } } }); } } } } fn fullscreen_request( &mut self, surface: ToplevelSurface, wl_output: Option, ) { // Uproszczenie MVP: pełny ekran = rozmiar wyjścia. Respektujemy protokół // i zawsze odpowiadamy configure. let output = wl_output .as_ref() .and_then(Output::from_resource) .or_else(|| self.space.outputs().next().cloned()); if let Some(output) = output { if let Some(geometry) = self.space.output_geometry(&output) { surface.with_pending_state(|state| { state.states.set(xdg_toplevel::State::Fullscreen); state.size = Some(geometry.size); }); } } surface.send_configure(); } fn unfullscreen_request(&mut self, surface: ToplevelSurface) { surface.with_pending_state(|state| { state.states.unset(xdg_toplevel::State::Fullscreen); state.size = None; }); surface.send_pending_configure(); } fn maximize_request(&mut self, surface: ToplevelSurface) { // Maksymalizacja w menedżerze pływającym = obszar ROBOCZY wyjścia // (bez panelu), żeby pasek systemowy pozostał widoczny. let Some(window) = self.window_for_surface(surface.wl_surface()) else { surface.send_configure(); return; }; if let Some(area) = self.output_work_area_for_window(&window) { surface.with_pending_state(|state| { state.states.set(xdg_toplevel::State::Maximized); state.size = Some(area.size); }); self.space.map_element(window, area.loc, true); } surface.send_configure(); } fn unmaximize_request(&mut self, surface: ToplevelSurface) { surface.with_pending_state(|state| { state.states.unset(xdg_toplevel::State::Maximized); state.size = None; }); surface.send_pending_configure(); } fn minimize_request(&mut self, surface: ToplevelSurface) { // Klient (np. nasz toolkit po kliknięciu przycisku) prosi o schowanie okna. // Robimy to samo, co skrót `Ctrl+Alt+M` — jedno miejsce prawdy. if let Some(window) = self.window_for_surface(surface.wl_surface()) { self.minimize_window(&window); } } fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) { // Popup (np. menu) prosi o grab wskaźnika/klawiatury. Musimy go ustanowić, // bo inaczej kliknięcie poza menu nie zamknie go. let Some(seat) = Seat::from_resource(&seat) else { return; }; let kind = PopupKind::Xdg(surface); let Some(root_surface) = find_popup_root_surface(&kind).ok() else { return; }; let Some(root_window) = self.window_for_surface(&root_surface) else { return; }; let root = KeyboardFocusTarget::Window(root_window.0.clone()); let Ok(mut grab) = self.popups.grab_popup(root, kind, &seat, serial) else { return; }; if let Some(keyboard) = seat.get_keyboard() { if keyboard.is_grabbed() && !(keyboard.has_grab(serial) || keyboard.has_grab(grab.previous_serial().unwrap_or(serial))) { grab.ungrab(PopupUngrabStrategy::All); return; } keyboard.set_focus(self, grab.current_grab(), serial); keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); } if let Some(pointer) = seat.get_pointer() { if pointer.is_grabbed() && !(pointer.has_grab(serial) || pointer.has_grab(grab.previous_serial().unwrap_or(serial))) { grab.ungrab(PopupUngrabStrategy::All); return; } pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep); } } } // --- Delegacje globali do naszego stanu ------------------------------------- // Te makra "spinają" globalne obiekty Waylanda z implementacjami traitów. delegate_compositor!(MyCompositor); delegate_shm!(MyCompositor); delegate_output!(MyCompositor); delegate_seat!(MyCompositor); delegate_xdg_shell!(MyCompositor); delegate_xdg_decoration!(MyCompositor); // --- Protokoły "biurkowe": schowek/DnD, aktywacja, skala, ograniczenia ------- impl DataDeviceHandler for MyCompositor { fn data_device_state(&self) -> &DataDeviceState { &self.data_device_state } } impl ClientDndGrabHandler for MyCompositor { fn started( &mut self, _source: Option, icon: Option, _seat: Seat, ) { // Ikona DnD: trzymamy powierzchnię; przesunięcie na razie zerowe (rysowanie // ikony podłączymy razem z kursorem z motywu XCursor). self.dnd_icon = icon.map(|surface| DndIcon { surface, offset: Point::from((0, 0)), }); } fn dropped(&mut self, _target: Option, _validated: bool, _seat: Seat) { self.dnd_icon = None; } } impl ServerDndGrabHandler for MyCompositor { fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat) { // Serwerowe graby DnD (np. przeciąganie pliku z panelu) nie są obsługiwane. warn!("serwerowy grab DnD nie jest obsługiwany"); } } // `SelectionHandler` jest częścią delegacji data-device — osobnego makra brak. impl SelectionHandler for MyCompositor { type SelectionUserData = (); } delegate_data_device!(MyCompositor); impl PrimarySelectionHandler for MyCompositor { fn primary_selection_state(&self) -> &PrimarySelectionState { &self.primary_selection_state } } delegate_primary_selection!(MyCompositor); impl XdgActivationHandler for MyCompositor { fn activation_state(&mut self) -> &mut XdgActivationState { &mut self.xdg_activation_state } fn token_created(&mut self, _token: XdgActivationToken, data: XdgActivationTokenData) -> bool { // Token uznajemy za ważny, jeśli pochodzi z ostatniego wejścia naszej klawiatury. if let Some((serial, seat)) = data.serial { let keyboard = self.seat.get_keyboard().unwrap(); Seat::from_resource(&seat) == Some(self.seat.clone()) && keyboard .last_enter() .map(|last_enter| serial.is_no_older_than(&last_enter)) .unwrap_or(false) } else { false } } fn request_activation( &mut self, _token: XdgActivationToken, token_data: XdgActivationTokenData, surface: WlSurface, ) { // Świeży token (<10 s) => spełniamy prośbę i podnosimy okno. if token_data.timestamp.elapsed().as_secs() < 10 { if let Some(window) = self.window_for_surface(&surface) { self.space.raise_element(&window, true); } } } } delegate_xdg_activation!(MyCompositor); impl FractionalScaleHandler for MyCompositor { fn new_fractional_scale(&mut self, surface: WlSurface) { // Ustalamy preferowaną skalę na podstawie wyjścia, na którym leży okno, // a jeśli nie wiemy — pierwszego wyjścia. Bez tego klienci HiDPI dostawaliby 1.0. let mut root = surface.clone(); while let Some(parent) = get_parent(&root) { root = parent; } let output = self.space.outputs().next().cloned().or_else(|| { self.window_for_surface(&root) .and_then(|window| self.space.outputs_for_element(&window).first().cloned()) }); if let Some(output) = output { let scale = output.current_scale().fractional_scale(); with_states(&surface, |states| { with_fractional_scale(states, |fractional| { fractional.set_preferred_scale(scale); }); }); } } } delegate_fractional_scale!(MyCompositor); impl PointerConstraintsHandler for MyCompositor { fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle) { // Aktywujemy ograniczenie tylko dla powierzchni, która ma fokus wskaźnika. let Some(current_focus) = pointer.current_focus() else { return; }; if current_focus.wl_surface().as_deref() == Some(surface) { with_pointer_constraint(surface, pointer, |constraint| { if let Some(constraint) = constraint { constraint.activate(); } }); } } fn cursor_position_hint( &mut self, surface: &WlSurface, pointer: &PointerHandle, location: Point, ) { let active = with_pointer_constraint(surface, pointer, |constraint| { constraint.is_some_and(|constraint| constraint.is_active()) }); if active { let origin = self .space .elements() .find_map(|window| { (window.wl_surface().as_deref() == Some(surface)).then(|| window.geometry()) }) .unwrap_or_default() .loc .to_f64(); pointer.set_location(origin + location); } } } delegate_pointer_constraints!(MyCompositor); // Text-input: sam global (bez input-method) — wystarcza, by klienty nie zgłaszały błędu. delegate_text_input_manager!(MyCompositor); // --- dmabuf, warstwy, viewporter, wskaźnik względny, gesty ----------------- impl DmabufHandler for MyCompositor { fn dmabuf_state(&mut self) -> &mut DmabufState { &mut self.dmabuf_state } fn dmabuf_imported( &mut self, _global: &DmabufGlobal, // `dmabuf` nieużywane bez feature `udev` (tam sprawdzamy import na GPU). #[cfg_attr(not(feature = "udev"), allow(unused_variables))] dmabuf: Dmabuf, notifier: ImportNotifier, ) { // W backendzie udev mamy renderery per GPU — sprawdzamy import naprawdę. #[cfg(feature = "udev")] { if let Some(udev) = self.udev.as_mut() { let node = udev.primary_gpu; if let Ok(mut renderer) = udev.gpus.single_renderer(&node) { if renderer.as_mut().import_dmabuf(&dmabuf, None).is_ok() { let _ = notifier.successful::(); } else { notifier.failed(); } return; } } } // Backend winit: renderer żyje lokalnie w pętli renderowania, więc nie // mamy go tutaj. Akceptujemy optymistycznie — global tworzymy z formatów // TEGO SAMEGO renderera, więc import przy renderowaniu się powiedzie. // WERYFIKUJ: przy problemach z dmabuf na winit przenieść backend do stanu. let _ = notifier.successful::(); } } delegate_dmabuf!(MyCompositor); impl WlrLayerShellHandler for MyCompositor { fn shell_state(&mut self) -> &mut WlrLayerShellState { &mut self.layer_shell_state } fn new_layer_surface( &mut self, surface: WlrLayerSurface, wl_output: Option, _layer: WlrLayer, namespace: String, ) { // Klient może wskazać wyjście, ale często tego nie robi — wtedy pierwsze. let output = wl_output .as_ref() .and_then(Output::from_resource) .or_else(|| self.space.outputs().next().cloned()); let Some(output) = output else { return }; let mut map = layer_map_for_output(&output); if let Err(err) = map.map_layer(&DesktopLayerSurface::new(surface, namespace)) { warn!(?err, "nie udało się zmapować layer surface"); } // Przelicz strefy (non-exclusive zone) po dodaniu warstwy. map.arrange(); } fn layer_destroyed(&mut self, surface: WlrLayerSurface) { if let Some((mut map, layer)) = self.space.outputs().find_map(|output| { let map = layer_map_for_output(output); let layer = map .layers() .find(|layer| layer.layer_surface() == &surface) .cloned(); layer.map(|layer| (map, layer)) }) { map.unmap_layer(&layer); map.arrange(); } } fn new_popup(&mut self, _parent: WlrLayerSurface, popup: PopupSurface) { if let Err(err) = self.popups.track_popup(PopupKind::from(popup)) { warn!(?err, "nie udało się śledzić popupu warstwy"); } } } delegate_layer_shell!(MyCompositor); delegate_viewporter!(MyCompositor); delegate_relative_pointer!(MyCompositor); delegate_pointer_gestures!(MyCompositor);