🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578
//! 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<i32, Logical>,
}
/// 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<i32, Logical>,
}
/// Główny stan kompozytora.
pub struct MyCompositor {
/// Przestrzeń okien — kolejność elementów to Z-order (od spodu do wierzchu).
pub space: Space<PaganWindow>,
/// Pozycja kursora w przestrzeni logicznej (lustrzana do stanu pointera).
pub pointer_location: Point<f64, Logical>,
/// Nazwa socketa Waylanda (dla klientów: `WAYLAND_DISPLAY`).
pub socket_name: Option<String>,
/// Flaga sterująca pętlą główną.
pub running: Arc<AtomicBool>,
/// 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<MyCompositor>,
pub xdg_shell_state: XdgShellState,
pub xdg_decoration_state: XdgDecorationState,
// Wejście.
pub seat: Seat<MyCompositor>,
pub pointer: PointerHandle<MyCompositor>,
/// 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<DndIcon>,
/// 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<MinimizedWindow>,
/// 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<crate::app_menu::protocol::server::pagan_app_menu_manager_v1::PaganAppMenuManagerV1>,
/// 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<Rectangle<i32, Logical>>,
// --- 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<DmabufGlobal>,
/// 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<crate::screencopy::PendingCapture>,
// --- 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<X11Wm>,
/// Numer wyświetlacza X (dla `DISPLAY=:N` uruchamianych aplikacji X11).
pub xdisplay: Option<u32>,
/// 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<crate::udev::UdevState>,
/// Następna pozycja dla kaskadowego rozmieszczania (współdzielona z `output.rs`).
pub(crate) next_cascade: Point<i32, Logical>,
}
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<Self>,
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::<Self>(&dh);
let shm_state = ShmState::new::<Self>(&dh, vec![]);
let output_manager_state = OutputManagerState::new_with_xdg_output::<Self>(&dh);
let xdg_shell_state = XdgShellState::new::<Self>(&dh);
let xdg_decoration_state = XdgDecorationState::new::<Self>(&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::<Self>(&dh);
let primary_selection_state = PrimarySelectionState::new::<Self>(&dh);
let xdg_activation_state = XdgActivationState::new::<Self>(&dh);
let fractional_scale_manager_state = FractionalScaleManagerState::new::<Self>(&dh);
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&dh);
// Text-input tworzymy tylko jako global (nie implementujemy jeszcze
// input-method), więc nie potrzebujemy przechowywać jego stanu.
TextInputManagerState::new::<Self>(&dh);
// Warstwy (panele/menus), viewporter oraz wskaźnik względny i gesty.
let layer_shell_state = WlrLayerShellState::new::<Self>(&dh);
ViewporterState::new::<Self>(&dh);
RelativePointerManagerState::new::<Self>(&dh);
PointerGesturesState::new::<Self>(&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::<Self>(&dh);
XWaylandKeyboardGrabState::new::<Self>(&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<Item = Format>) {
if self.dmabuf_global.is_some() {
return;
}
let global = self
.dmabuf_state
.create_global::<Self>(&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<PaganWindow> {
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::<KeyboardFocusTarget>::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<PaganWindow> {
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<Rectangle<i32, Logical>> {
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<Rectangle<i32, Logical>> {
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<i32, Logical>) {
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::<XdgToplevelSurfaceData>()
.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::<RefCell<SurfaceData>>() {
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<Self>,
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<Self>,
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::<RefCell<SurfaceData>>()
.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::<XWaylandClientData>() {
return &state.compositor_state;
}
if let Some(state) = client.get_data::<ClientState>() {
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::<Self>(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::<SurfaceAttributes>()
.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::<SurfaceAttributes>()
.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::<LayerSurfaceData>()
.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<Self> {
&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<Self>, 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<Self>, 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::<RefCell<SurfaceData>>()
.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::<XdgToplevelSurfaceData>()
.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::<RefCell<SurfaceData>>() {
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<wl_output::WlOutput>,
) {
// 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<WlDataSource>,
icon: Option<WlSurface>,
_seat: Seat<Self>,
) {
// 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<WlSurface>, _validated: bool, _seat: Seat<Self>) {
self.dnd_icon = None;
}
}
impl ServerDndGrabHandler for MyCompositor {
fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat<Self>) {
// 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<Self>) {
// 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<Self>,
location: Point<f64, Logical>,
) {
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::<Self>();
} 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::<Self>();
}
}
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<wl_output::WlOutput>,
_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);