//! Backend produkcyjny: **tty-udev** (DRM/KMS + EGL/GBM + libinput + libseat). //! //! PO CO: to ścieżka "prawdziwego" środowiska graficznego — kompozytor przejmuje //! konsolę (TTY), sam ustawia tryby wyświetlania przez DRM/KMS i czyta wejście //! bezpośrednio z libinput, z uprawnieniami zarządzanymi przez libseat (logind //! albo seatd). W przeciwieństwie do winit nie działa "w oknie". //! //! Moduł jest włączany feature'em `udev` (domyślnie wyłączonym), bo wymaga //! bibliotek systemowych: libdrm, libgbm, libinput, libudev, libseat. //! //! ⚠️ STATUS: kod napisany według sprawdzonych wzorców z `smithay-0.7.0/anvil` //! (`run_udev`, `device_added`, `connector_connected`, pętla VBlank), ale NIE był //! kompilowany ani uruchamiany na sprzęcie. Pierwsze uruchomienie wymaga TTY //! (nie sesji graficznej) i zwykle uprawnień: użyj logind/libseat, albo uruchom //! z innego TTY jako root. Miejsca szczególnie warte weryfikacji oznaczono //! komentarzami `// WERYFIKUJ:`. use std::{ collections::HashMap, path::{Path, PathBuf}, sync::atomic::Ordering, time::Duration, }; use rustix::fs::OFlags; use tracing::{error, info, warn}; use smithay::{ backend::{ allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, drm::{ compositor::FrameFlags, exporter::gbm::GbmFramebufferExporter, output::{DrmOutput, DrmOutputManager, DrmOutputRenderElements}, DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, NodeType, }, egl::{context::ContextPriority, EGLDevice, EGLDisplay}, input::{InputBackend, InputEvent}, libinput::{LibinputInputBackend, LibinputSessionInterface}, renderer::{ gles::GlesRenderer, multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer}, ImportDma, ImportMemWl, }, session::{libseat::LibSeatSession, Event as SessionEvent, Session}, udev::{primary_gpu, UdevBackend, UdevEvent}, }, output::{Mode as WlMode, Output, PhysicalProperties, Subpixel}, reexports::{ calloop::{EventLoop, RegistrationToken}, drm::control::{connector, crtc, ModeTypeFlags}, input::Libinput, wayland_server::{backend::GlobalId, Display}, }, utils::{Clock, DeviceFd, Monotonic, Point, Transform}, }; // `DrmScanner` przeniesiono z Smithay do osobnego crate'a `smithay-drm-extras`. use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; use crate::{render, state::MyCompositor}; /// Renderer łączący API renderujący z API alokującym (multi-GPU). #[allow(dead_code)] type UdevRenderer<'a> = MultiRenderer< 'a, 'a, GbmGlesBackend, GbmGlesBackend, >; /// Konkretny typ kompozytora DRM/GBM (skrót dokumentacyjny). #[allow(dead_code)] type GbmDrmCompositor = smithay::backend::drm::compositor::DrmCompositor< GbmAllocator, GbmFramebufferExporter, (), DrmDeviceFd, >; /// Identyfikuje wyjście po (urządzenie DRM, CRTC) — wkładane do `user_data` Output. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct UdevOutputId { pub device_id: DrmNode, pub crtc: crtc::Handle, } /// Stan backendu tty-udev. Trzymany w `MyCompositor::udev`. pub struct UdevState { /// Uchwyt sesji (libseat) — wspólny dla DRM i libinput. pub session: LibSeatSession, /// Nazwa seatu (np. "seat0") — do `udev_assign_seat`. pub seat_name: String, /// Preferowane GPU (z niego bierzemy renderer domyślny). pub primary_gpu: DrmNode, /// Menedżer rendererów per GPU. pub gpus: GpuManager>, /// Urządzenia DRM (klucz: node), każde z własnym menedżerem wyjść. pub backends: HashMap, /// Kursor rysowany przez kompozytor (wspólny dla wszystkich wyjść). pub pointer_element: render::PointerElement, /// Zegar monotoniczny (m.in. do znaczników czasu klatek). pub clock: Clock, } /// Dane jednego urządzenia DRM. pub struct UdevDevice { pub drm_output_manager: DrmOutputManager< GbmAllocator, GbmFramebufferExporter, (), DrmDeviceFd, >, pub drm_scanner: DrmScanner, pub render_node: Option, pub surfaces: HashMap, /// Uchwyt rejestracji notifiera DRM w pętli — trzymany, żeby żył. pub registration_token: RegistrationToken, } /// Dane pojedynczego wyjścia (CRTC) na urządzeniu DRM. pub struct UdevSurfaceData { pub output: Output, pub drm_output: DrmOutput, GbmFramebufferExporter, (), DrmDeviceFd>, pub global: Option, } /// Punkt wejścia backendu tty-udev. pub fn run_udev() -> anyhow::Result<()> { let mut event_loop = EventLoop::::try_new()?; let display = Display::::new()?; // 1) Sesja przez libseat — daje nam uprawnienia do DRM, VT i urządzeń wejścia. let (session, notifier) = LibSeatSession::new() .map_err(|err| anyhow::anyhow!("nie udało się otworzyć sesji libseat: {err}"))?; let seat_name = session.seat(); // 2) Wybór GPU. Preferujemy jawny override przez PAGAN_DRM_DEVICE. let primary_gpu = if let Ok(var) = std::env::var("PAGAN_DRM_DEVICE") { DrmNode::from_path(var).map_err(|err| anyhow::anyhow!("PAGAN_DRM_DEVICE: {err}"))? } else { primary_gpu(&seat_name)? .and_then(|path| { DrmNode::from_path(path) .ok()? .node_with_type(NodeType::Render)? .ok() }) .ok_or_else(|| { anyhow::anyhow!("nie znaleziono żadnego GPU (searched seat {seat_name})") })? }; info!(%primary_gpu, "PaganDE: wybrano GPU"); // 3) Menedżer rendererów (openGL przez glow, kontekst o wysokim priorytecie). let mut gpus = GpuManager::new(GbmGlesBackend::with_context_priority(ContextPriority::High)) .map_err(|err| anyhow::anyhow!("GpuManager: {err:?}"))?; // Formaty SHM/dmabuf bierzemy z renderera ZANIM `gpus` trafi do stanu. let (shm_formats, dmabuf_formats) = { let renderer = gpus .single_renderer(&primary_gpu) .map_err(|err| anyhow::anyhow!("renderer: {err:?}"))?; (renderer.shm_formats(), renderer.dmabuf_formats()) }; // 4) Stan protokołów kompozytora (bez wyjścia — dodamy je przy device_added). let mut state = MyCompositor::init(display, event_loop.handle(), true); state.shm_state.update_formats(shm_formats); state.enable_dmabuf(dmabuf_formats); // XWayland (aplikacje X11). state.start_xwayland(); state.attach_udev(UdevState { session: session.clone(), seat_name: seat_name.clone(), primary_gpu, gpus, backends: HashMap::new(), pointer_element: render::PointerElement::default(), clock: Clock::new(), }); // 5) libinput — wejście. Podłączony do sesji, więc automatycznie respektuje // pauzę/aktywację VT. let mut libinput_context = Libinput::new_with_udev::>(session.clone().into()); libinput_context .udev_assign_seat(&seat_name) .map_err(|_| anyhow::anyhow!("libinput: nie udało się przypisać seatu {seat_name}"))?; let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); event_loop .handle() .insert_source( libinput_backend, move |event, _, data: &mut MyCompositor| { data.process_input_event(event); }, ) .map_err(|err| anyhow::anyhow!("rejestracja libinput: {err}"))?; // 6) Pauza/wznowienie sesji (przełączanie VT, suspend). { let mut libinput_context = libinput_context.clone(); event_loop .handle() .insert_source( notifier, move |event, &mut (), data: &mut MyCompositor| match event { SessionEvent::PauseSession => { libinput_context.suspend(); info!("sesja wstrzymana — pauzuję wyjścia DRM"); if let Some(udev) = data.udev.as_mut() { for device in udev.backends.values_mut() { device.drm_output_manager.pause(); } } } SessionEvent::ActivateSession => { info!("sesja wznowiona — aktywuję wyjścia DRM"); if let Err(err) = libinput_context.resume() { error!(?err, "libinput: nie udało się wznowić"); } // WERYFIKUJ: `activate(false)` zachowuje stan connectorów; gdy // sterownik tego nie lubi, zmień na `activate(true)`. let nodes: Vec = data .udev .as_ref() .map(|udev| udev.backends.keys().copied().collect()) .unwrap_or_default(); for node in nodes { if let Some(udev) = data.udev.as_mut() { if let Some(device) = udev.backends.get_mut(&node) { if let Err(err) = device.drm_output_manager.activate(false) { error!(?err, %node, "nie udało się aktywować DRM"); } } } data.handle .insert_idle(move |state| state.udev_render(node)); } } }, ) .map_err(|err| anyhow::anyhow!("rejestracja notifiera sesji: {err}"))?; } // 7) Udev — wykrywanie urządzeń DRM i hotplug. let udev_backend = UdevBackend::new(&seat_name).map_err(|err| anyhow::anyhow!("UdevBackend: {err}"))?; // Najpierw dodajemy urządzenia już obecne (żeby zdążyły zainicjalizować GPU). let existing: Vec<(u64, PathBuf)> = udev_backend .device_list() .map(|(device_id, path)| (device_id as u64, path.to_path_buf())) .collect(); for (device_id, path) in existing { if let Ok(node) = DrmNode::from_dev_id(device_id as _) { if let Err(err) = state.udev_device_added(node, &path) { warn!(%node, ?err, "pomijam urządzenie DRM"); } } } event_loop .handle() .insert_source( udev_backend, move |event, _, data: &mut MyCompositor| match event { UdevEvent::Added { device_id, path } => { if let Ok(node) = DrmNode::from_dev_id(device_id) { if let Err(err) = data.udev_device_added(node, &path) { warn!(%node, ?err, "nie udało się dodać urządzenia DRM"); } } } UdevEvent::Changed { device_id } => { if let Ok(node) = DrmNode::from_dev_id(device_id) { data.udev_device_changed(node); } } UdevEvent::Removed { device_id } => { if let Ok(node) = DrmNode::from_dev_id(device_id) { data.udev_device_removed(node); } } }, ) .map_err(|err| anyhow::anyhow!("rejestracja UdevBackend: {err}"))?; info!("PaganDE (tty-udev) wystartował. Zakończ Ctrl+C lub przełącz VT."); // 8) Pętla główna. Rendering jest sterowany zdarzeniami (VBlank → render). while state.running.load(Ordering::SeqCst) { if let Err(err) = event_loop.dispatch(Some(Duration::from_millis(16)), &mut state) { error!(?err, "błąd pętli zdarzeń"); state.running.store(false, Ordering::SeqCst); continue; } state.space.refresh(); // Lista okien dla paneli (tytuły, stan aktywny/zminimalizowany). state.sync_foreign_toplevels(); state.popups.cleanup(); let _ = state.display_handle.flush_clients(); } Ok(()) } /// Błąd dodawania urządzenia DRM. #[derive(Debug)] pub enum DeviceAddError { DeviceOpen(String), DrmDevice(smithay::backend::drm::DrmError), GbmDevice(std::io::Error), AddNode(String), } impl std::fmt::Display for DeviceAddError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DeviceAddError::DeviceOpen(err) => write!(f, "otwarcie urządzenia: {err}"), DeviceAddError::DrmDevice(err) => write!(f, "DrmDevice: {err:?}"), DeviceAddError::GbmDevice(err) => write!(f, "GbmDevice: {err}"), DeviceAddError::AddNode(err) => write!(f, "dodanie node'a GPU: {err}"), } } } impl std::error::Error for DeviceAddError {} impl MyCompositor { /// Inicjalizuje urządzenie DRM: otwiera je, tworzy GBM, alokator, exporter, /// menedżer wyjść oraz rejestruje notifier VBlank. pub fn udev_device_added(&mut self, node: DrmNode, path: &Path) -> Result<(), DeviceAddError> { let (mut session, primary_gpu) = match self.udev.as_ref() { Some(udev) => (udev.session.clone(), udev.primary_gpu), None => return Err(DeviceAddError::AddNode("brak stanu udev".into())), }; // Otwarcie urządzenia przez sesję (nadaje uprawnienia DRM master). let fd = session .open( path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, ) .map_err(|err| DeviceAddError::DeviceOpen(format!("{err:?}")))?; let fd = DrmDeviceFd::new(DeviceFd::from(fd)); let (drm, notifier) = DrmDevice::new(fd.clone(), true).map_err(DeviceAddError::DrmDevice)?; let gbm = GbmDevice::new(fd).map_err(DeviceAddError::GbmDevice)?; // Notifier VBlank: po każdym wyświetleniu klatki planujemy kolejny render. let registration_token = self .handle .insert_source( notifier, move |event, _metadata, data: &mut MyCompositor| match event { DrmEvent::VBlank(crtc) => data.udev_frame_finish(node, crtc), DrmEvent::Error(err) => error!(?err, "błąd DRM"), }, ) .map_err(|err| DeviceAddError::AddNode(err.to_string()))?; // Zbudowanie kontekstu EGL dla tego urządzenia i rejestracja renderera. // WERYFIKUJ: `EGLDisplay::new` jest `unsafe` (surowe uchwyty EGL). let display = unsafe { EGLDisplay::new(gbm.clone()).map_err(|err| DeviceAddError::AddNode(err.to_string()))? }; let egl_device = EGLDevice::device_for_display(&display) .map_err(|err| DeviceAddError::AddNode(err.to_string()))?; let render_node = egl_device .try_get_render_node() .ok() .flatten() .unwrap_or(node); { let udev = self .udev .as_mut() .ok_or(DeviceAddError::AddNode("brak stanu udev".into()))?; udev.gpus .as_mut() .add_node(render_node, gbm.clone()) .map_err(|err| DeviceAddError::AddNode(err.to_string()))?; } let allocator = GbmAllocator::new( gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, ); let framebuffer_exporter = GbmFramebufferExporter::new(gbm.clone(), Some(render_node)); // Zbiór formatów renderowania wspieranych przez EGL (do alokacji buforów). let render_formats = { let udev = self .udev .as_mut() .ok_or(DeviceAddError::AddNode("brak stanu udev".into()))?; let mut renderer = udev .gpus .single_renderer(&render_node) .map_err(|err| DeviceAddError::AddNode(format!("{err:?}")))?; renderer .as_mut() .egl_context() .dmabuf_render_formats() .iter() .copied() .collect::>() }; let drm_output_manager = DrmOutputManager::new( drm, allocator, framebuffer_exporter, Some(gbm), [ smithay::backend::allocator::Fourcc::Argb8888, smithay::backend::allocator::Fourcc::Xrgb8888, ], render_formats, ); let udev = self .udev .as_mut() .ok_or(DeviceAddError::AddNode("brak stanu udev".into()))?; udev.backends.insert( node, UdevDevice { drm_output_manager, drm_scanner: DrmScanner::new(), render_node: Some(render_node), surfaces: HashMap::new(), registration_token, }, ); let _ = primary_gpu; // Skanujemy już podłączone monitory. self.udev_device_changed(node); Ok(()) } /// Reakcja na zmianę urządzenia: skanuje konektory i dodaje/usuwa wyjścia. pub fn udev_device_changed(&mut self, node: DrmNode) { let scan_result = { let Some(udev) = self.udev.as_mut() else { return; }; let Some(device) = udev.backends.get_mut(&node) else { return; }; match device .drm_scanner .scan_connectors(device.drm_output_manager.device()) { Ok(result) => result, Err(err) => { warn!(?err, %node, "skanowanie konektorów nie powiodło się"); return; } } }; for event in scan_result { match event { DrmScanEvent::Connected { connector, crtc: Some(crtc), } => { self.udev_connector_connected(node, connector, crtc); } DrmScanEvent::Disconnected { connector: _, crtc: Some(crtc), } => { self.udev_connector_disconnected(node, crtc); } _ => {} } } } /// Tworzy wyjście Waylanda i kompozytor DRM dla podłączonego konektora. pub fn udev_connector_connected( &mut self, node: DrmNode, connector: connector::Info, crtc: crtc::Handle, ) { let output_name = format!( "{}-{}", connector.interface().as_str(), connector.interface_id() ); // Wybierz tryb preferowany (albo pierwszy dostępny). let mode_id = connector .modes() .iter() .position(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) .unwrap_or(0); let Some(drm_mode) = connector.modes().get(mode_id).copied() else { warn!(%output_name, "konektor bez trybów"); return; }; let wl_mode = WlMode::from(drm_mode); // Układ: nowe wyjście na prawo od już istniejących. let x = self.space.outputs().fold(0, |acc, output| { acc + self .space .output_geometry(output) .map(|g| g.size.w) .unwrap_or(0) }); let position = Point::from((x, 0)); let output = Output::new( output_name.clone(), PhysicalProperties { // Fizyczny rozmiar panelu (mm) — informacyjny dla klientów. size: connector .size() .map(|(w, h)| (w as i32, h as i32)) .unwrap_or((0, 0)) .into(), subpixel: Subpixel::Unknown, make: "PaganDE".into(), model: "DRM".into(), }, ); let global = output.create_global::(&self.display_handle); output.set_preferred(wl_mode); output.change_current_state(Some(wl_mode), Some(Transform::Normal), None, Some(position)); self.space.map_output(&output, position); output.user_data().insert_if_missing(|| UdevOutputId { device_id: node, crtc, }); // Ustawiamy wszystkie wyjścia poziomo (bez przerw) i ratujemy okna, // jeśli nowy monitor zmienił układ. self.relayout_outputs(); // Renderer dla tego GPU. let Some(udev) = self.udev.as_mut() else { return; }; let render_node = udev .backends .get(&node) .and_then(|device| device.render_node) .unwrap_or(udev.primary_gpu); let mut renderer = match udev.gpus.single_renderer(&render_node) { Ok(renderer) => renderer, Err(err) => { warn!(?err, "brak renderera dla wyjścia"); return; } }; let Some(device) = udev.backends.get_mut(&node) else { return; }; let planes = device.drm_output_manager.device().planes(&crtc).ok(); let drm_output = match device .drm_output_manager .initialize_output::( crtc, drm_mode, &[connector.handle()], &output, planes, renderer.as_mut(), &DrmOutputRenderElements::default(), ) { Ok(drm_output) => drm_output, Err(err) => { warn!(?err, %output_name, "nie udało się zainicjalizować wyjścia DRM"); return; } }; device.surfaces.insert( crtc, UdevSurfaceData { output: output.clone(), drm_output, global: Some(global), }, ); info!(%output_name, ?crtc, "skonfigurowano wyjście"); // Pierwsza klatka. self.handle .insert_idle(move |state| state.udev_render(node)); } /// Usuwa wyjście dla odłączonego konektora. pub fn udev_connector_disconnected(&mut self, node: DrmNode, crtc: crtc::Handle) { // Zdejmujemy pożyczkę na `self.udev` w bloku, żeby potem móc wywołać // `relayout_outputs` (metoda przyjmująca całe `&mut self`). let removed = { let Some(udev) = self.udev.as_mut() else { return; }; let Some(device) = udev.backends.get_mut(&node) else { return; }; device.surfaces.remove(&crtc) }; let Some(surface) = removed else { return }; if let Some(global) = surface.global.as_ref() { self.display_handle .remove_global::(global.clone()); } self.space.unmap_output(&surface.output); info!(?crtc, "usunięto wyjście"); // Re-layout bez przerwy i przeniesienie okien z usuniętego ekranu. self.relayout_outputs(); } /// Usuwa całe urządzenie DRM (odłączone/utracone). pub fn udev_device_removed(&mut self, node: DrmNode) { let removed = { let Some(udev) = self.udev.as_mut() else { return; }; udev.backends.remove(&node) }; let Some(device) = removed else { return }; for surface in device.surfaces.values() { if let Some(global) = surface.global.as_ref() { self.display_handle .remove_global::(global.clone()); } self.space.unmap_output(&surface.output); } info!(%node, "usunięto urządzenie DRM"); self.relayout_outputs(); } /// Renderuje i kolejkuje klatkę na wszystkich wyjściach danego urządzenia. pub fn udev_render(&mut self, node: DrmNode) { let crtcs: Vec = self .udev .as_ref() .and_then(|udev| udev.backends.get(&node)) .map(|device| device.surfaces.keys().copied().collect()) .unwrap_or_default(); for crtc in crtcs { self.udev_render_surface(node, crtc); } } /// Renderuje jedną klatkę wyjścia i — jeśli coś się zmieniło — kolejkuje ją. pub fn udev_render_surface(&mut self, node: DrmNode, crtc: crtc::Handle) { let Some(output) = self.udev_output(node, crtc) else { return; }; let render_node = { let Some(udev) = self.udev.as_ref() else { return; }; udev.backends .get(&node) .and_then(|device| device.render_node) .unwrap_or(udev.primary_gpu) }; let pointer_location = self.pointer_location; let cursor_status = self.cursor_status.clone(); let Some(udev) = self.udev.as_mut() else { return; }; // Kursor WYJMUJEMY na czas renderu: `render_elements` potrzebuje `&mut`, // a jednocześnie pożyczamy `udev.gpus` i `udev.backends` — wyjęcie elementu // rozplątuje te pożyczki bez walki z borrow checkerem. let mut pointer_element = std::mem::take(&mut udev.pointer_element); pointer_element.update(render::pointer_physical(pointer_location), true); pointer_element.set_status(cursor_status); let mut renderer = match udev.gpus.single_renderer(&render_node) { Ok(renderer) => renderer, Err(err) => { warn!(?err, "brak renderera podczas renderowania"); udev.pointer_element = pointer_element; return; } }; let Some(device) = udev.backends.get_mut(&node) else { udev.pointer_element = pointer_element; return; }; let Some(surface) = device.surfaces.get_mut(&crtc) else { udev.pointer_element = pointer_element; return; }; // Nasz renderer jest zbudowany na GlesRenderer (pixel shadery paska SSD), a // `MultiRenderer` udostępnia wewnętrzny renderer przez `AsMut`. let gles: &mut GlesRenderer = renderer.as_mut(); let (elements, clear_color) = render::output_elements( &self.space, &output, Some(&mut pointer_element), self.dnd_icon.as_ref(), self.snap_preview, pointer_location, gles, ); match surface .drm_output .render_frame(gles, &elements, clear_color, FrameFlags::DEFAULT) { Ok(result) => { if !result.is_empty { if let Err(err) = surface.drm_output.queue_frame(()) { warn!(?err, "nie udało się zakolejkować klatki"); } } } Err(err) => warn!(?err, "błąd renderowania wyjścia"), } // Oddajemy kursor z powrotem do stanu backendu. udev.pointer_element = pointer_element; } /// Po VBlank: kończymy klatkę i planujemy kolejny render. pub fn udev_frame_finish(&mut self, node: DrmNode, crtc: crtc::Handle) { let Some(udev) = self.udev.as_mut() else { return; }; if let Some(device) = udev.backends.get_mut(&node) { if let Some(surface) = device.surfaces.get_mut(&crtc) { // Przetwarza zakończoną klatkę (zwalnia bufory, domyka kolejki). let _ = surface.drm_output.frame_submitted(); } } // Zakolejkowane przechwycenia ekranu (wlr-screencopy) — mamy tu renderer. if !self.screencopy_queue.is_empty() { let render_node = self .udev .as_ref() .map(|udev| udev.primary_gpu) .unwrap_or(node); if let Some(udev) = self.udev.as_mut() { if let Ok(mut renderer) = udev.gpus.single_renderer(&render_node) { crate::screencopy::process( renderer.as_mut(), &self.space, self.pointer_location, &mut self.screencopy_queue, ); } } } // Zaplanuj kolejną klatkę. Prosty, ciągły rendering na VBlank wystarcza // na start; optymalizacja to renderowanie tylko przy uszkodzeniach. self.handle .insert_idle(move |state| state.udev_render(node)); } /// Znajduje `Output` odpowiadające parze (urządzenie, CRTC). fn udev_output(&self, node: DrmNode, crtc: crtc::Handle) -> Option { self.space .outputs() .find(|output| { output.user_data().get::() == Some(&UdevOutputId { device_id: node, crtc, }) }) .cloned() } /// Przetwarza zdarzenie wejścia z libinput (backend udev). /// /// PO CO osobno od `process_input_event_windowed`: libinput podaje ruch /// WZGLĘDNY (delty), a winit absolutny. Obsługa przycisków/scrolla/klawiatury /// jest wspólna (`input.rs`). pub fn process_input_event(&mut self, event: InputEvent) { match event { InputEvent::Keyboard { event } => self.handle_keyboard::(event), InputEvent::PointerMotion { event } => self.on_pointer_move::(event), InputEvent::PointerButton { event } => self.on_pointer_button::(event), InputEvent::PointerAxis { event } => self.on_pointer_axis::(event), _ => {} } } }