← luna

Commit f1c2102

4
files
+440
added
-5
deleted
@@ -60,6 +60,9 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
60 60 „dumb”, framebuffer (`ADDFB2`), `SETCRTC` i **page-flip** na wykrytych monitorach; render
61 61 sceny wprost do buforów (podwójne buforowanie). Gdy master trzyma inna sesja — bezpieczny
62 62 fallback do nested (nie odbiera ekranu).
63 +- **Wejście evdev** (`SELENE_INPUT=evdev`, opcjonalne): własne czytanie `/dev/input/event*`
64 + (wskaźnik względny z wirtualnym kursorem, przyciski, kółko, klawiatura) — rodzime wejście
65 + dla trybu DRM, bez nadrzędnego kompozytora. Wymaga dostępu do grupy `input`/root.
63 66 - Wielojęzyczność (`libluna-i18n`) i optymalizacja (`[profile.release]` LTO, cache `shm`).
64 67
65 68 ## Powłoka Luna / Luna shell (`de/luna`)
@@ -81,8 +84,11 @@ Selene.
81 84
82 85 ## Jeszcze nie / Not yet
83 86
84 -- **Wejście w trybie DRM** (evdev/`libinput`) — skanowanie działa, ale bez rodzimego wejścia
85 - okna są tylko wyświetlane; GLES/akceleracja i hotplug (`zwlr_output_manager`) — do zrobienia.
87 +- **Wejście evdev** (`evdev.rs`): zaimplementowane i sterowane `SELENE_INPUT=evdev`, ale
88 + wymaga dostępu do `/dev/input` (grupa `input`/root); bez tego Selene zgłasza
89 + `evdev input unavailable`.
90 +- GLES/akceleracja, hotplug monitorów (`zwlr_output_manager`), `libinput` (gesty touchpada,
91 + akceleracja) zamiast surowych zdarzeń.
86 92 - **Selekcja pierwotna** (`primary selection`); IME / `text-input`.
87 93 - `wl_output` dynamiczne (zmiana trybu), fokus klawiatury po kliknięciu poza oknem.
88 94 - Render tylko przy zmianie modelu (częściowe odświeżanie/damage).
@@ -102,6 +108,7 @@ Tryb nested (domyślny) uruchamia okno na nadrzędnym kompozytorze. **Realne wyj
102 108 ```sh
103 109 SELENE_BACKEND=drm XDG_RUNTIME_DIR="$PWD/.selene-runtime" cargo run -p selene
104 110 # diagnostyka bez zmiany trybu: SELENE_DRM_PROBE=1 cargo run -p selene
111 +# rodzime wejście (gdy brak nadrzędnego kompozytora): SELENE_INPUT=evdev cargo run -p selene
105 112 ```
106 113
107 114 Uwaga: tryb DRM przejmuje ekran (page-flip); gdy master trzyma Twoja sesja, Selene
@@ -125,6 +132,7 @@ de/selene/src/
125 132 monitors.rs # wl_output (model wielu monitorów, układ, trafienia)
126 133 drm.rs # wykrywanie + scanout DRM/KMS (ioctl, dumb buffer, page-flip), fallback nested
127 134 seat.rs # wl_seat: wskaźnik + klawiatura + trasowanie wejścia i operacje okien
135 + evdev.rs # wejście z /dev/input/event* (tryb DRM, wirtualny kursor)
128 136 input.rs # magistrala wejścia (kolejka + wybudzanie)
129 137 output.rs # wyjście nested (libluna-platform + libluna-render)
130 138 ```
131 139 new file mode 100644
@@ -0,0 +1,377 @@
1 +//! Wejście z urządzeń `evdev` (`/dev/input/event*`) — rodzime wejście dla trybu DRM.
2 +//!
3 +//! W trybie nested wejście przychodzi od nadrzędnego kompozytora. Gdy Selene działa
4 +//! bezpośrednio na DRM/KMS, nie ma rodzica — wtedy czytamy zdarzenia wprost z urządzeń
5 +//! wejściowych i tłumaczymy je na [`libluna_core::Event`] (ten sam strumień, który
6 +//! trafia do `seat::handle_input`).
7 +//!
8 +//! Zakres: wskaźnik (względny), przyciski myszy, kółko i klawiatura. Śledzimy wirtualny
9 +//! kursor w kompozytorze (evdev daje przemieszczenia, nie pozycję bezwzględną).
10 +
11 +use std::os::fd::{AsRawFd, OwnedFd};
12 +
13 +use libluna_core::{
14 + ButtonState, Event, KeyEvent, Modifiers, Point, PointerButton, PointerEvent, ScrollEvent,
15 +};
16 +use rustix::fs::{Mode, OFlags};
17 +
18 +// Typy zdarzeń evdev.
19 +const EV_SYN: u16 = 0x00;
20 +const EV_KEY: u16 = 0x01;
21 +const EV_REL: u16 = 0x02;
22 +const SYN_REPORT: u16 = 0;
23 +const REL_HWHEEL: u16 = 0x06;
24 +const REL_WHEEL: u16 = 0x08;
25 +const REL_X: u16 = 0x00;
26 +const REL_Y: u16 = 0x01;
27 +
28 +/// `struct input_event` (64-bit): `timeval` (2×i64) + typ + kod + wartość.
29 +#[repr(C)]
30 +#[derive(Clone, Copy, Default)]
31 +struct RawEvent {
32 + time_sec: i64,
33 + time_usec: i64,
34 + type_: u16,
35 + code: u16,
36 + value: i32,
37 +}
38 +
39 +/// Jedno urządzenie wejściowe.
40 +struct Device {
41 + fd: OwnedFd,
42 + #[allow(dead_code)]
43 + path: String,
44 +}
45 +
46 +/// Wejście evdev: zestaw urządzeń + wirtualny kursor.
47 +pub struct EvdevInput {
48 + devices: Vec<Device>,
49 + cursor: Point,
50 + acc_dx: f64,
51 + acc_dy: f64,
52 + acc_scroll_x: f64,
53 + acc_scroll_y: f64,
54 +}
55 +
56 +impl EvdevInput {
57 + /// Otwiera wszystkie czytelne urządzenia `/dev/input/event*`.
58 + /// Zwraca `None`, gdy nie ma dostępnych urządzeń (np. brak uprawnień).
59 + pub fn open() -> Option<Self> {
60 + let dir = std::fs::read_dir("/dev/input").ok()?;
61 + let mut paths: Vec<String> = dir
62 + .flatten()
63 + .map(|e| e.path())
64 + .filter(|p| {
65 + p.file_name()
66 + .map(|n| n.to_string_lossy().starts_with("event"))
67 + .unwrap_or(false)
68 + })
69 + .map(|p| p.to_string_lossy().into_owned())
70 + .collect();
71 + paths.sort();
72 +
73 + let mut devices = Vec::new();
74 + for path in paths {
75 + let Ok(fd) = rustix::fs::open(
76 + path.as_str(),
77 + OFlags::RDONLY | OFlags::NONBLOCK | OFlags::CLOEXEC,
78 + Mode::empty(),
79 + ) else {
80 + continue;
81 + };
82 + devices.push(Device { fd, path });
83 + }
84 + if devices.is_empty() {
85 + return None;
86 + }
87 + Some(Self {
88 + devices,
89 + cursor: Point::new(0.0, 0.0),
90 + acc_dx: 0.0,
91 + acc_dy: 0.0,
92 + acc_scroll_x: 0.0,
93 + acc_scroll_y: 0.0,
94 + })
95 + }
96 +
97 + /// Surowe deskryptory do `poll` (do wykrywania gotowości).
98 + pub fn fds(&self) -> Vec<std::os::fd::RawFd> {
99 + self.devices.iter().map(|d| d.fd.as_raw_fd()).collect()
100 + }
101 +
102 + /// Czyta i tłumaczy wszystkie oczekujące zdarzenia.
103 + ///
104 + /// `bounds` to rozmiar monitora — kursor jest do niego ograniczany.
105 + pub fn read_events(&mut self, bounds: (f64, f64)) -> Vec<Event> {
106 + // Najpierw odczyt (pożyczka współdzielona urządzeń), potem tłumaczenie (mutacja).
107 + let mut decoded = Vec::new();
108 + for dev in &self.devices {
109 + let mut buf = [0u8; std::mem::size_of::<RawEvent>() * 64];
110 + loop {
111 + match rustix::io::read(&dev.fd, &mut buf) {
112 + Ok(0) => break,
113 + Ok(n) => {
114 + for chunk in buf[..n].chunks_exact(std::mem::size_of::<RawEvent>()) {
115 + if let Some(ev) = decode(chunk) {
116 + decoded.push(ev);
117 + }
118 + }
119 + }
120 + Err(_) => break, // EAGAIN lub błąd — kończymy na tym urządzeniu
121 + }
122 + }
123 + }
124 + let mut out = Vec::new();
125 + for ev in decoded {
126 + self.apply(ev, bounds, &mut out);
127 + }
128 + out
129 + }
130 +
131 + fn apply(&mut self, ev: Decoded, bounds: (f64, f64), out: &mut Vec<Event>) {
132 + match ev {
133 + Decoded::Motion(dx, dy) => {
134 + self.acc_dx += dx;
135 + self.acc_dy += dy;
136 + }
137 + Decoded::Scroll(dx, dy) => {
138 + self.acc_scroll_x += dx;
139 + self.acc_scroll_y += dy;
140 + }
141 + Decoded::Button(button, pressed) => {
142 + let state = if pressed {
143 + ButtonState::Pressed
144 + } else {
145 + ButtonState::Released
146 + };
147 + out.push(Event::PointerButton(PointerEvent {
148 + position: self.cursor,
149 + button,
150 + state,
151 + modifiers: Modifiers::NONE,
152 + }));
153 + }
154 + Decoded::Key(keysym, pressed) => {
155 + let state = if pressed {
156 + ButtonState::Pressed
157 + } else {
158 + ButtonState::Released
159 + };
160 + out.push(Event::Key(KeyEvent {
161 + keysym,
162 + text: None,
163 + state,
164 + modifiers: Modifiers::NONE,
165 + repeat: false,
166 + }));
167 + }
168 + Decoded::Syn => {
169 + if self.acc_dx != 0.0 || self.acc_dy != 0.0 {
170 + self.cursor = Point::new(
171 + (self.cursor.x + self.acc_dx).clamp(0.0, (bounds.0 - 1.0).max(0.0)),
172 + (self.cursor.y + self.acc_dy).clamp(0.0, (bounds.1 - 1.0).max(0.0)),
173 + );
174 + self.acc_dx = 0.0;
175 + self.acc_dy = 0.0;
176 + out.push(Event::PointerMotion {
177 + position: self.cursor,
178 + modifiers: Modifiers::NONE,
179 + });
180 + }
181 + if self.acc_scroll_x != 0.0 || self.acc_scroll_y != 0.0 {
182 + out.push(Event::Scroll(ScrollEvent {
183 + position: self.cursor,
184 + dx: self.acc_scroll_x,
185 + dy: -self.acc_scroll_y,
186 + }));
187 + self.acc_scroll_x = 0.0;
188 + self.acc_scroll_y = 0.0;
189 + }
190 + }
191 + }
192 + }
193 +}
194 +
195 +/// Zdarzenie po przetłumaczeniu z bajtów.
196 +#[derive(Debug, Clone, Copy, PartialEq)]
197 +enum Decoded {
198 + Motion(f64, f64),
199 + Scroll(f64, f64),
200 + Button(PointerButton, bool),
201 + Key(u32, bool),
202 + Syn,
203 +}
204 +
205 +/// Dekoduje jeden `input_event`.
206 +fn decode(bytes: &[u8]) -> Option<Decoded> {
207 + if bytes.len() < std::mem::size_of::<RawEvent>() {
208 + return None;
209 + }
210 + let ev = RawEvent {
211 + time_sec: i64::from_ne_bytes(bytes[0..8].try_into().ok()?),
212 + time_usec: i64::from_ne_bytes(bytes[8..16].try_into().ok()?),
213 + type_: u16::from_ne_bytes(bytes[16..18].try_into().ok()?),
214 + code: u16::from_ne_bytes(bytes[18..20].try_into().ok()?),
215 + value: i32::from_ne_bytes(bytes[20..24].try_into().ok()?),
216 + };
217 + match ev.type_ {
218 + EV_SYN if ev.code == SYN_REPORT => Some(Decoded::Syn),
219 + EV_REL => match ev.code {
220 + REL_X => Some(Decoded::Motion(ev.value as f64, 0.0)),
221 + REL_Y => Some(Decoded::Motion(0.0, ev.value as f64)),
222 + REL_HWHEEL => Some(Decoded::Scroll(ev.value as f64, 0.0)),
223 + REL_WHEEL => Some(Decoded::Scroll(0.0, ev.value as f64)),
224 + _ => None,
225 + },
226 + EV_KEY => {
227 + let pressed = ev.value != 0;
228 + match ev.code {
229 + 0x110 => Some(Decoded::Button(PointerButton::Left, pressed)),
230 + 0x111 => Some(Decoded::Button(PointerButton::Right, pressed)),
231 + 0x112 => Some(Decoded::Button(PointerButton::Middle, pressed)),
232 + 0x115 => Some(Decoded::Button(PointerButton::Forward, pressed)),
233 + 0x116 => Some(Decoded::Button(PointerButton::Back, pressed)),
234 + code => evdev_to_keysym(code).map(|ks| Decoded::Key(ks, pressed)),
235 + }
236 + }
237 + _ => None,
238 + }
239 +}
240 +
241 +/// Kod evdev → keysym (podzbiór wspólny z mapą klawiatury Selene).
242 +fn evdev_to_keysym(code: u16) -> Option<u32> {
243 + // Litery (evdev: a=30 … l=38, m=50 …; użyto standardowego układu evdev).
244 + let keysym = match code {
245 + 1 => 0xff1b, // Escape
246 + 14 => 0xff08, // BackSpace
247 + 15 => 0xff09, // Tab
248 + 28 => 0xff0d, // Return
249 + 57 => 0x0020, // space
250 + 103 => 0xff52, // Up
251 + 105 => 0xff51, // Left
252 + 106 => 0xff53, // Right
253 + 108 => 0xff54, // Down
254 + 110 => 0xff50, // Home
255 + 111 => 0xffff, // Delete
256 + 115 => 0xff57, // End
257 + // cyfry 1..0
258 + 2 => 0x0031,
259 + 3 => 0x0032,
260 + 4 => 0x0033,
261 + 5 => 0x0034,
262 + 6 => 0x0035,
263 + 7 => 0x0036,
264 + 8 => 0x0037,
265 + 9 => 0x0038,
266 + 10 => 0x0039,
267 + 11 => 0x0030,
268 + // litery
269 + 30 => 0x0061, // a
270 + 48 => 0x0062, // b
271 + 46 => 0x0063, // c
272 + 32 => 0x0064, // d
273 + 18 => 0x0065, // e
274 + 33 => 0x0066, // f
275 + 34 => 0x0067, // g
276 + 35 => 0x0068, // h
277 + 23 => 0x0069, // i
278 + 36 => 0x006a, // j
279 + 37 => 0x006b, // k
280 + 38 => 0x006c, // l
281 + 50 => 0x006d, // m
282 + 49 => 0x006e, // n
283 + 24 => 0x006f, // o
284 + 25 => 0x0070, // p
285 + 16 => 0x0071, // q
286 + 19 => 0x0072, // r
287 + 31 => 0x0073, // s
288 + 20 => 0x0074, // t
289 + 22 => 0x0075, // u
290 + 47 => 0x0076, // v
291 + 17 => 0x0077, // w
292 + 45 => 0x0078, // x
293 + 21 => 0x0079, // y
294 + 44 => 0x007a, // z
295 + // modyfikatory
296 + 42 | 54 => 0xffe1, // Shift
297 + 29 | 97 => 0xffe3, // Control
298 + 56 | 100 => 0xffe9, // Alt
299 + 125 | 126 => 0xffeb, // Super
300 + _ => return None,
301 + };
302 + Some(keysym)
303 +}
304 +
305 +#[cfg(test)]
306 +mod tests {
307 + use super::*;
308 +
309 + fn raw(type_: u16, code: u16, value: i32) -> [u8; 24] {
310 + let mut b = [0u8; 24];
311 + b[16..18].copy_from_slice(&type_.to_ne_bytes());
312 + b[18..20].copy_from_slice(&code.to_ne_bytes());
313 + b[20..24].copy_from_slice(&value.to_ne_bytes());
314 + b
315 + }
316 +
317 + #[test]
318 + fn raw_event_layout_matches_kernel() {
319 + assert_eq!(std::mem::size_of::<RawEvent>(), 24);
320 + }
321 +
322 + #[test]
323 + fn decodes_button_and_key_and_rel() {
324 + assert_eq!(
325 + decode(&raw(EV_KEY, 0x110, 1)),
326 + Some(Decoded::Button(PointerButton::Left, true))
327 + );
328 + assert_eq!(
329 + decode(&raw(EV_KEY, 30, 0)),
330 + Some(Decoded::Key(0x0061, false))
331 + );
332 + assert_eq!(
333 + decode(&raw(EV_REL, REL_X, 5)),
334 + Some(Decoded::Motion(5.0, 0.0))
335 + );
336 + assert_eq!(decode(&raw(EV_SYN, SYN_REPORT, 0)), Some(Decoded::Syn));
337 + assert_eq!(decode(&raw(EV_KEY, 0x2ff, 1)), None); // nieznany klawisz
338 + }
339 +
340 + #[test]
341 + fn accumulates_motion_and_emits_on_syn() {
342 + let mut input = EvdevInput {
343 + devices: Vec::new(),
344 + cursor: Point::new(100.0, 100.0),
345 + acc_dx: 0.0,
346 + acc_dy: 0.0,
347 + acc_scroll_x: 0.0,
348 + acc_scroll_y: 0.0,
349 + };
350 + let mut out = Vec::new();
351 + input.apply(Decoded::Motion(10.0, 5.0), (1280.0, 800.0), &mut out);
352 + assert!(out.is_empty()); // jeszcze bez SYN
353 + input.apply(Decoded::Syn, (1280.0, 800.0), &mut out);
354 + assert_eq!(out.len(), 1);
355 + assert!(
356 + matches!(out[0], Event::PointerMotion { position, .. } if position.x == 110.0 && position.y == 105.0)
357 + );
358 + }
359 +
360 + #[test]
361 + fn cursor_is_clamped_to_bounds() {
362 + let mut input = EvdevInput {
363 + devices: Vec::new(),
364 + cursor: Point::new(1270.0, 0.0),
365 + acc_dx: 0.0,
366 + acc_dy: 0.0,
367 + acc_scroll_x: 0.0,
368 + acc_scroll_y: 0.0,
369 + };
370 + let mut out = Vec::new();
371 + input.apply(Decoded::Motion(1000.0, -50.0), (1280.0, 800.0), &mut out);
372 + input.apply(Decoded::Syn, (1280.0, 800.0), &mut out);
373 + assert!(
374 + matches!(out[0], Event::PointerMotion { position, .. } if position.x == 1279.0 && position.y == 0.0)
375 + );
376 + }
377 +}
@@ -9,6 +9,7 @@ mod compositor;
9 9 mod data;
10 10 mod decoration;
11 11 mod drm;
12 +mod evdev;
12 13 mod input;
13 14 mod layer;
14 15 mod monitors;
@@ -110,6 +111,22 @@ fn main() -> Result<(), Box<dyn Error>> {
110 111
111 112 let listener_fd = socket.as_raw_fd();
112 113
114 + // Wejście evdev (dla trybu DRM, gdzie nie ma nadrzędnego kompozytora).
115 + let mut evdev_input = if std::env::var("SELENE_INPUT").as_deref() == Ok("evdev") {
116 + match evdev::EvdevInput::open() {
117 + Some(input) => {
118 + println!("{}", libluna_i18n::tr("evdev input enabled"));
119 + Some(input)
120 + }
121 + None => {
122 + eprintln!("{}", libluna_i18n::tr("evdev input unavailable"));
123 + None
124 + }
125 + }
126 + } else {
127 + None
128 + };
129 +
113 130 loop {
114 131 let backend_fd = display.backend().poll_fd().as_raw_fd();
115 132 let wake_fd = wake_read.as_raw_fd();
@@ -117,13 +134,43 @@ fn main() -> Result<(), Box<dyn Error>> {
117 134 let listener = unsafe { BorrowedFd::borrow_raw(listener_fd) };
118 135 let backend = unsafe { BorrowedFd::borrow_raw(backend_fd) };
119 136 let wake = unsafe { BorrowedFd::borrow_raw(wake_fd) };
120 - let mut fds = [
137 + let input_raw = evdev_input.as_ref().map(|e| e.fds()).unwrap_or_default();
138 + // SAFETY: deskryptory należą do `evdev_input`, który żyje przez całą iterację.
139 + let input_fds: Vec<BorrowedFd> = input_raw
140 + .iter()
141 + .map(|&r| unsafe { BorrowedFd::borrow_raw(r) })
142 + .collect();
143 + let mut fds = vec![
121 144 rustix::event::PollFd::new(&listener, rustix::event::PollFlags::IN),
122 145 rustix::event::PollFd::new(&backend, rustix::event::PollFlags::IN),
123 146 rustix::event::PollFd::new(&wake, rustix::event::PollFlags::IN),
124 147 ];
148 + for fd in &input_fds {
149 + fds.push(rustix::event::PollFd::new(fd, rustix::event::PollFlags::IN));
150 + }
125 151 rustix::event::poll(&mut fds, -1)?;
126 152
153 + // Wejście evdev → trasowanie (jak z magistrali).
154 + if input_fds
155 + .iter()
156 + .enumerate()
157 + .any(|(i, _)| fds[3 + i].revents().contains(rustix::event::PollFlags::IN))
158 + {
159 + if let Some(input) = evdev_input.as_mut() {
160 + let bounds = state
161 + .outputs
162 + .lock()
163 + .unwrap()
164 + .first()
165 + .map(|o| (o.width as f64, o.height as f64))
166 + .unwrap_or((1280.0, 800.0));
167 + let batch = input.read_events(bounds);
168 + for event in &batch {
169 + seat::handle_input(&mut state, &handle, event);
170 + }
171 + }
172 + }
173 +
127 174 // Nowy klient.
128 175 if fds[0].revents().contains(rustix::event::PollFlags::IN) {
129 176 while let Ok(Some(stream)) = socket.accept() {
@@ -156,10 +156,13 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
156 156 oraz **realne wyjście DRM/KMS** (`SELENE_BACKEND=drm`, opcjonalne): master, bufor dumb,
157 157 `ADDFB2`, `SETCRTC`, podwójne buforowanie i **page-flip**; bezpieczny fallback do nested,
158 158 gdy master trzyma inna sesja.
159 + ✅ M-K8: **wejście evdev** (`SELENE_INPUT=evdev`): własne czytanie `/dev/input/event*`
160 + (wskaźnik względny z wirtualnym kursorem, przyciski, kółko, klawiatura) — rodzime wejście
161 + dla trybu DRM. Wymaga grupy `input`/root.
159 162 ✅ Optymalizacje: cache `mmap` puli `wl_shm` i zero-copy w rendererze (`PixmapRef::from_bytes`).
160 163 Uruchomienie: `cargo run -p selene` (szczegóły: `de/README.md`).
161 -- Dalej: **wejście w trybie DRM** (evdev/`libinput`), GLES/akceleracja, hotplug monitorów
162 - (`zwlr_output_manager`), selekcja pierwotna, IME (`text-input`).
164 +- Dalej: GLES/akceleracja, hotplug monitorów (`zwlr_output_manager`), `libinput` (gesty),
165 + selekcja pierwotna, IME (`text-input`).
163 166 - **Luna** (`de/luna`) — powłoka środowiska na `libluna-widgets`. ✅ start: **panel górny**
164 167 (menu, tytuł okna, zegar/data, przełączniki, przestrzenie, użytkownik) i **launcher**
165 168 (siatka aplikacji + wyszukiwanie); render headless do `docs/images/luna-shell-{light,dark}.png`.