← luna

Commit 0122c49

9
files
+741
added
-50
deleted
@@ -19,9 +19,10 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
19 19 | Środowisko | **Luna** | pulpit (panel, launcher, ustawienia) — `de/luna` (wkrótce) |
20 20 | Kompozytor | **Selene** | serwer Wayland — `de/selene` |
21 21
22 -## Co już działa / What works (M-K1)
22 +## Co już działa / What works (M-K2)
23 23
24 -- Globalne interfejsy: `wl_compositor` (v4), `wl_shm`, `xdg_wm_base` (v1), `wl_seat` (v1).
24 +- Globalne interfejsy: `wl_compositor` (v4), `wl_shm`, **`wl_output`** (v4), `xdg_wm_base` (v1),
25 + **`wl_seat`** (v1) z **wskaźnikiem i klawiaturą**.
25 26 - Cykl życia powierzchni: `wl_surface` (`attach`/`damage`/`frame`/`commit`), `wl_callback`
26 27 (wywołanie `done` przy commicie), `wl_region`.
27 28 - `wl_shm`: `create_pool` → `wl_shm_pool.create_buffer` → `wl_buffer`; piksele bufora są
@@ -29,21 +30,29 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
29 30 - xdg-shell: `get_xdg_surface` → `get_toplevel`, pierwszy `commit` wysyła `configure`
30 31 (oraz `xdg_toplevel.configure`), `ack_configure`, `set_title`/`set_app_id`.
31 32 - **Wyjście nested**: `libluna-platform` otwiera okno na nadrzędnym kompozytorze (osobny
32 - wątek), a `libluna-render` komponuje scenę (tło + okna klientów + etykieta). `libluna-render`
33 - ma teraz prymityw `Scene::image` (obrazy/ piksele) i `ImageData`.
34 -- Minimalny `wl_seat` (obiekty wejścia tworzone, bez zdarzeń).
35 -- Wielojęzyczność: lokalizacja z otoczenia + `i18n/` ładowane w runtime (`libluna-i18n`).
36 -- Optymalizacja: `[profile.release]` z pełnym LTO i 1 jednostką kodegenu.
33 + wątek), a `libluna-render` komponuje scenę (tło + okna w kolejności `z`). Renderer ma
34 + prymityw `Scene::image`/`ImageData`.
35 +- **Wejście**: magistrala (kolejka + para gniazd do wybudzania `poll`). Wątek wyjścia
36 + przekazuje zdarzenia od rodzica; protokół trasuje je do klientów:
37 + `wl_pointer` (`enter`/`motion`/`button`/`axis`/`leave`) i `wl_keyboard` (`keymap`
38 + z wbudowanej mapy XKB, `enter`/`leave`/`key`/`modifiers` z syntezą modyfikatorów).
39 +- **Zarządzanie oknami (pływające)**: trafienie w okno = fokus + podniesienie (`z`),
40 + przeciągnięcie lewym przyciskiem przenosi okno; kaskada przy starcie.
41 +- **Monitory**: model `Vec<OutputInfo>` (gotowy na wiele); okno nested = jeden monitor
42 + (rozmiar śledzony z konfiguracji okna).
43 +- Wielojęzyczność (`libluna-i18n`) i optymalizacja (`[profile.release]` LTO).
37 44
38 45 ## Jeszcze nie / Not yet
39 46
40 -- **Wejście**: zdarzenia klawiatury/wskaźnika (`libinput`/evdev) i `wl_pointer`/`wl_keyboard`;
41 - dziś okna są widoczne, ale nieinteraktywne.
42 -- **Zarządzanie oknami**: pozycjonowanie jest tymczasowe (kaskada), brak fokusu/warstw/układu.
43 -- Protokoły dodatkowe: `xdg_popup`, `wl_output`, `layer-shell`, `xdg-decoration`, schowek.
44 -- Optymalizacje: mapa `wl_shm` cache'owana (dziś `mmap` na commit), unikanie klonowania
45 - pikseli na klatkę (cache `ImageData`), render tylko przy zmianie modelu.
46 -- Backend sprzętowy: DRM/KMS + GLES (dziś: software + nested).
47 +- **Wiele monitorów**: realne wyjścia DRM/KMS i przypisywanie okien do monitorów (dziś jeden).
48 +- **Dekoracje/zachowanie okien**: brak `xdg-decoration`, resize krawędziami, minimalizacji,
49 + pełnego ekranu, `xdg_popup`, `layer-shell`.
50 +- **Klawiatura**: mapa `us` przez systemowe dane XKB (`/usr/share/X11/xkb`); inne układy,
51 + IME i `repeat_info` — do zrobienia.
52 +- Schowek (`wl_data_device`), `wl_output` dynamiczne (zmiana trybu), fokus klawiatury na klik
53 + poza oknem, SnapEdge’y/układ.
54 +- Optymalizacje: cache mapy `shm`, brak klonowania pikseli na klatkę (cache `ImageData`),
55 + render tylko przy zmianie modelu. Backend sprzętowy (DRM/KMS + GLES).
47 56
48 57 ## Uruchomienie / Running
49 58
@@ -61,11 +70,13 @@ katalog (renderowanie pojawi się, gdy wejdzie backend wyświetlania).
61 70
62 71 ```
63 72 de/selene/src/
64 - main.rs # display, globale, gniazdo, pętla poll
65 - state.rs # stan współdzielony + model okien (współdzielony z wyjściem)
73 + main.rs # display, globale, gniazdo, pętla poll (nasłuch + backend + magistrala)
74 + state.rs # stan: model okien, z-order, fokus, drag, monitory
66 75 compositor.rs # wl_compositor / wl_surface / wl_region / wl_callback + kompozycja
67 76 shm.rs # wl_shm / wl_shm_pool / wl_buffer (+ odczyt pikseli)
68 77 shell.rs # xdg_wm_base / xdg_surface / xdg_toplevel / xdg_positioner
69 - seat.rs # wl_seat (minimalnie)
78 + monitors.rs # wl_output (model wielu monitorów)
79 + seat.rs # wl_seat: wskaźnik + klawiatura + trasowanie wejścia
80 + input.rs # magistrala wejścia (kolejka + wybudzanie)
70 81 output.rs # wyjście nested (libluna-platform + libluna-render)
71 82 ```
@@ -108,7 +108,7 @@ impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
108 108 fn request(
109 109 state: &mut Self,
110 110 _client: &Client,
111 - _resource: &wl_surface::WlSurface,
111 + resource: &wl_surface::WlSurface,
112 112 request: wl_surface::Request,
113 113 data: &SurfaceData,
114 114 _handle: &DisplayHandle,
@@ -131,7 +131,7 @@ impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
131 131 // Pierwszy commit powierzchni z `xdg_surface` → wyślij konfigurację.
132 132 maybe_configure_xdg(state, data);
133 133 // Skomponuj okno (jeśli to toplevel z buforem).
134 - composite(state, data);
134 + composite(state, resource, data);
135 135 }
136 136 _ => {}
137 137 }
@@ -139,7 +139,7 @@ impl Dispatch<wl_surface::WlSurface, SurfaceData> for State {
139 139 }
140 140
141 141 /// Skomponuj powierzchnię do modelu okien, jeśli jest to toplevel z buforem `wl_shm`.
142 -fn composite(state: &mut State, data: &SurfaceData) {
142 +fn composite(state: &mut State, surface: &wl_surface::WlSurface, data: &SurfaceData) {
143 143 let Some(xdg_surface) = data.xdg_surface.lock().unwrap().clone() else {
144 144 return;
145 145 };
@@ -161,9 +161,11 @@ fn composite(state: &mut State, data: &SurfaceData) {
161 161 return;
162 162 };
163 163 let image = std::sync::Arc::new(ImageData::new(w as u32, h as u32, rgba));
164 + let z = state.next_z();
164 165
165 166 let mut slot = data.slot.lock().unwrap();
166 167 let mut windows = state.windows.lock().unwrap();
168 + let mut created = false;
167 169 let idx = match *slot {
168 170 Some(i) => i,
169 171 None => {
@@ -173,9 +175,11 @@ fn composite(state: &mut State, data: &SurfaceData) {
173 175 y: 60.0 + i as f64 * 36.0,
174 176 width: w,
175 177 height: h,
178 + z,
176 179 data: image.clone(),
177 180 });
178 181 *slot = Some(i);
182 + created = true;
179 183 i
180 184 }
181 185 };
@@ -186,6 +190,10 @@ fn composite(state: &mut State, data: &SurfaceData) {
186 190 }
187 191 drop(windows);
188 192 drop(slot);
193 + // Zarejestruj powierzchnię okna (indeks = slot) do trasowania wejścia.
194 + if created && state.slots.len() == idx {
195 + state.slots.push(surface.clone());
196 + }
189 197 // Pozwól klientowi ponownie użyć bufora.
190 198 let _ = buffer.send_event(wl_buffer::Event::Release);
191 199 }
192 200 new file mode 100644
@@ -0,0 +1,37 @@
1 +//! Magistrala wejścia: kolejka zdarzeń + para gniazd do wybudzania pętli głównej.
2 +//!
3 +//! Wątek wyjścia (osobne połączenie Wayland) odbiera zdarzenia wejścia od nadrzędnego
4 +//! kompozytora i przekazuje je tutaj; wątek protokołu opróżnia kolejkę i trasuje je do
5 +//! klientów (`wl_pointer`/`wl_keyboard`).
6 +
7 +use std::io::{Read, Write};
8 +use std::os::unix::net::UnixStream;
9 +use std::sync::{Arc, Mutex};
10 +
11 +use libluna_core::Event;
12 +
13 +/// Współdzielona kolejka zdarzeń wejścia.
14 +pub type Events = Arc<Mutex<Vec<Event>>>;
15 +
16 +/// Tworzy kolejkę i parę gniazd `(read, write)` do wybudzania.
17 +pub fn channel() -> (Events, UnixStream, UnixStream) {
18 + let (read, write) = UnixStream::pair().expect("socketpair wejścia");
19 + (Arc::new(Mutex::new(Vec::new())), read, write)
20 +}
21 +
22 +/// Dokłada partię zdarzeń i budzi pętlę główną (jeden bajt).
23 +pub fn push(events: &Events, wake: &mut UnixStream, batch: &[Event]) {
24 + if batch.is_empty() {
25 + return;
26 + }
27 + events.lock().unwrap().extend_from_slice(batch);
28 + let _ = wake.write_all(&[1u8]);
29 +}
30 +
31 +/// Opróżnia bajt wybudzający (nieblokująco).
32 +pub fn drain_wake(wake: &mut UnixStream) {
33 + let _ = wake.set_nonblocking(true);
34 + let mut buf = [0u8; 64];
35 + let _ = wake.read(&mut buf);
36 + let _ = wake.set_nonblocking(false);
37 +}
@@ -1,10 +1,13 @@
1 1 //! Selene — kompozytor Wayland środowiska Luna.
2 2 //!
3 3 //! Świadomie **bez smithay**: protokół obsługujemy własną warstwą na `wayland-server`
4 -//! (rdzeń + xdg-shell), a pętla zdarzeń to bezpośredni `poll` po gnieździe nasłuchu
5 -//! i deskryptorze backendu. Renderowanie docelowo przez `libluna-render`.
4 +//! (rdzeń + xdg-shell + output + seat), a pętla zdarzeń to bezpośredni `poll` po gnieździe
5 +//! nasłuchu, deskryptorze backendu i magistrali wejścia. Wyjście (nested) i kompozycja
6 +//! idą przez `libluna-platform` + `libluna-render`.
6 7
7 8 mod compositor;
9 +mod input;
10 +mod monitors;
8 11 mod output;
9 12 mod seat;
10 13 mod shell;
@@ -15,8 +18,9 @@ use std::error::Error;
15 18 use std::os::fd::{AsRawFd, BorrowedFd};
16 19 use std::sync::Arc;
17 20
21 +use libluna_core::Event;
18 22 use wayland_protocols::xdg::shell::server::xdg_wm_base;
19 -use wayland_server::protocol::{wl_compositor, wl_seat, wl_shm};
23 +use wayland_server::protocol::{wl_compositor, wl_output, wl_seat, wl_shm};
20 24 use wayland_server::{Display, ListeningSocket};
21 25
22 26 use state::State;
@@ -32,6 +36,7 @@ fn main() -> Result<(), Box<dyn Error>> {
32 36 // Globalne interfejsy protokołu.
33 37 handle.create_global::<State, wl_compositor::WlCompositor, ()>(4, ());
34 38 handle.create_global::<State, wl_shm::WlShm, ()>(1, ());
39 + handle.create_global::<State, wl_output::WlOutput, ()>(4, ());
35 40 handle.create_global::<State, xdg_wm_base::XdgWmBase, ()>(1, ());
36 41 handle.create_global::<State, wl_seat::WlSeat, ()>(1, ());
37 42
@@ -49,22 +54,36 @@ fn main() -> Result<(), Box<dyn Error>> {
49 54
50 55 let mut state = State::default();
51 56
57 + // Magistrala wejścia (wątek wyjścia → wątek protokołu) z parą gniazd do wybudzania.
58 + let (events, mut wake_read, wake_write) = input::channel();
59 +
52 60 // Wyjście nested: okno na nadrzędnym kompozytorze, budowane w osobnym wątku.
53 - output::spawn(std::sync::Arc::clone(&state.windows), 1280, 800);
61 + output::spawn(
62 + Arc::clone(&state.windows),
63 + Arc::clone(&state.outputs),
64 + Arc::clone(&events),
65 + wake_write,
66 + 1280,
67 + 800,
68 + );
54 69
55 70 let listener_fd = socket.as_raw_fd();
56 71
57 72 loop {
58 73 let backend_fd = display.backend().poll_fd().as_raw_fd();
74 + let wake_fd = wake_read.as_raw_fd();
59 75
60 76 let listener = unsafe { BorrowedFd::borrow_raw(listener_fd) };
61 77 let backend = unsafe { BorrowedFd::borrow_raw(backend_fd) };
78 + let wake = unsafe { BorrowedFd::borrow_raw(wake_fd) };
62 79 let mut fds = [
63 80 rustix::event::PollFd::new(&listener, rustix::event::PollFlags::IN),
64 81 rustix::event::PollFd::new(&backend, rustix::event::PollFlags::IN),
82 + rustix::event::PollFd::new(&wake, rustix::event::PollFlags::IN),
65 83 ];
66 84 rustix::event::poll(&mut fds, -1)?;
67 85
86 + // Nowy klient.
68 87 if fds[0].revents().contains(rustix::event::PollFlags::IN) {
69 88 while let Ok(Some(stream)) = socket.accept() {
70 89 handle.insert_client(stream, Arc::new(()))?;
@@ -72,6 +91,18 @@ fn main() -> Result<(), Box<dyn Error>> {
72 91 }
73 92 }
74 93
94 + // Wejście z magistrali → trasowanie do klientów.
95 + if fds[2].revents().contains(rustix::event::PollFlags::IN) {
96 + input::drain_wake(&mut wake_read);
97 + let batch: Vec<Event> = {
98 + let mut queue = events.lock().unwrap();
99 + std::mem::take(&mut *queue)
100 + };
101 + for event in &batch {
102 + seat::handle_input(&mut state, event);
103 + }
104 + }
105 +
75 106 display.dispatch_clients(&mut state)?;
76 107 display.flush_clients()?;
77 108 }
78 109 new file mode 100644
@@ -0,0 +1,90 @@
1 +//! `wl_output` — monitory (na razie jeden; model gotowy na wiele).
2 +
3 +use std::sync::{Arc, Mutex};
4 +
5 +use wayland_server::protocol::wl_output::{self, WlOutput};
6 +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
7 +
8 +use crate::state::State;
9 +
10 +/// Informacje o monitorze (współdzielone z wątkiem wyjścia, który zna bieżący rozmiar).
11 +#[derive(Debug, Clone)]
12 +pub struct OutputInfo {
13 + pub name: String,
14 + pub width: i32,
15 + pub height: i32,
16 + pub scale: i32,
17 +}
18 +
19 +impl Default for OutputInfo {
20 + fn default() -> Self {
21 + Self {
22 + name: "nested-0".to_owned(),
23 + width: 1280,
24 + height: 800,
25 + scale: 1,
26 + }
27 + }
28 +}
29 +
30 +/// Współdzielona lista monitorów.
31 +pub type Outputs = Arc<Mutex<Vec<OutputInfo>>>;
32 +
33 +/// Wysyła klientowi opis monitora (geometria, tryb, skala, nazwa, gotowe).
34 +pub fn send_info(output: &WlOutput, info: &OutputInfo) {
35 + let _ = output.send_event(wl_output::Event::Geometry {
36 + x: 0,
37 + y: 0,
38 + physical_width: info.width * 300 / 1000,
39 + physical_height: info.height * 300 / 1000,
40 + subpixel: wayland_server::WEnum::Value(wl_output::Subpixel::Unknown),
41 + make: "Luna".to_owned(),
42 + model: info.name.clone(),
43 + transform: wayland_server::WEnum::Value(wl_output::Transform::Normal),
44 + });
45 + let _ = output.send_event(wl_output::Event::Mode {
46 + flags: wayland_server::WEnum::Value(wl_output::Mode::Current),
47 + width: info.width,
48 + height: info.height,
49 + refresh: 60_000,
50 + });
51 + let _ = output.send_event(wl_output::Event::Scale { factor: info.scale });
52 + let _ = output.send_event(wl_output::Event::Name {
53 + name: info.name.clone(),
54 + });
55 + let _ = output.send_event(wl_output::Event::Done);
56 +}
57 +
58 +impl GlobalDispatch<WlOutput, ()> for State {
59 + fn bind(
60 + state: &mut Self,
61 + _handle: &DisplayHandle,
62 + _client: &Client,
63 + resource: New<WlOutput>,
64 + _data: &(),
65 + data_init: &mut DataInit<'_, Self>,
66 + ) {
67 + let output = data_init.init(resource, ());
68 + let info = state
69 + .outputs
70 + .lock()
71 + .unwrap()
72 + .first()
73 + .cloned()
74 + .unwrap_or_default();
75 + send_info(&output, &info);
76 + }
77 +}
78 +
79 +impl Dispatch<WlOutput, ()> for State {
80 + fn request(
81 + _state: &mut Self,
82 + _client: &Client,
83 + _resource: &WlOutput,
84 + _request: wl_output::Request,
85 + _data: &(),
86 + _handle: &DisplayHandle,
87 + _data_init: &mut DataInit<'_, Self>,
88 + ) {
89 + }
90 +}
@@ -1,23 +1,46 @@
1 1 //! Wyjście nested: Selene otwiera okno na nadrzędnym kompozytorze i rysuje scenę okien.
2 2 //!
3 -//! Wykorzystujemy `libluna-platform` (strona klienta Wayland) jako backend wyświetlania —
4 -//! dzięki temu nie duplikujemy obsługi `wl_shm`/xdg-shell po stronie klienta. Wątek wyjścia
5 -//! odświeża scenę w pętli (przez `Frame::request_frame`) i komponuje okna z modelu.
3 +//! Wątek wyjścia pełni dwie role: (1) rysuje skomponowaną scenę przez `libluna-render`
4 +//! i (2) odbiera wejście od nadrzędnego kompozytora, przekazując je do magistrali
5 +//! (`crate::input`), skąd wątek protokołu trasuje je do klientów.
6 +
7 +use std::os::unix::net::UnixStream;
6 8
7 9 use libluna_core::{Color, Point, Rect, ThemeSettings};
8 10 use libluna_platform::{run, AppConfig, Frame};
9 11 use libluna_render::TextRun;
10 12
11 -use crate::state::WindowModel;
13 +use crate::input::{self, Events};
14 +use crate::monitors::Outputs;
15 +use crate::state::{Window, WindowModel};
12 16
13 17 /// Uruchamia wątek wyjścia (nested Wayland) o zadanym rozmiarze.
14 -pub fn spawn(model: WindowModel, width: u32, height: u32) {
18 +pub fn spawn(
19 + model: WindowModel,
20 + outputs: Outputs,
21 + events: Events,
22 + wake: UnixStream,
23 + width: u32,
24 + height: u32,
25 +) {
15 26 std::thread::spawn(move || {
27 + let mut wake = wake;
16 28 let config = AppConfig::new("Selene")
17 29 .app_id("eu.paganlinux.Luna.Selene")
18 30 .initial_size(width, height)
19 31 .settings(ThemeSettings::default());
20 32 let _ = run(config, move |frame: &mut Frame| {
33 + // Zgłoś bieżący rozmiar monitora (okno może być zmieniane przez rodzica).
34 + {
35 + let mut outs = outputs.lock().unwrap();
36 + if let Some(out) = outs.first_mut() {
37 + out.width = frame.size.width as i32;
38 + out.height = frame.size.height as i32;
39 + }
40 + }
41 + // Przekaż wejście do magistrali.
42 + input::push(&events, &mut wake, frame.events);
43 + // Narysuj scenę.
21 44 draw(frame, &model);
22 45 // Utrzymuj klatki — model okien zmienia się poza tą pętlą.
23 46 frame.request_frame();
@@ -26,16 +49,17 @@ pub fn spawn(model: WindowModel, width: u32, height: u32) {
26 49 }
27 50
28 51 fn draw(frame: &mut Frame, model: &WindowModel) {
29 - let size = frame.size;
30 52 frame.scene.clear();
31 53 frame.scene.fill_rect(
32 - Rect::from_size(size),
54 + Rect::from_size(frame.size),
33 55 Color::from_rgba8(0x1e, 0x20, 0x28, 0xff),
34 56 );
35 57
36 58 {
37 59 let windows = model.lock().unwrap();
38 - for w in windows.iter() {
60 + let mut order: Vec<&Window> = windows.iter().collect();
61 + order.sort_by_key(|w| w.z);
62 + for w in order {
39 63 frame.scene.image(
40 64 Rect::from_xywh(w.x, w.y, w.width as f64, w.height as f64),
41 65 w.data.clone(),
@@ -1,11 +1,29 @@
1 -//! `wl_seat` — minimalne wejście (na razie bez zdarzeń; obiekty tworzone, by klienci działali).
1 +//! `wl_seat` — wejście: wskaźnik (`wl_pointer`), klawiatura (`wl_keyboard`) i trasowanie.
2 +//!
3 +//! Zdarzenia wejścia pochodzą z magistrali (`crate::input`) — przekazuje je wątek wyjścia,
4 +//! który odbiera je od nadrzędnego kompozytora. Tutaj tłumaczymy je na zdarzenia protokołu
5 +//! wysyłane do sfokusowanych powierzchni klientów.
2 6
7 +use std::io::{Seek, Write};
8 +use std::os::fd::{AsFd, OwnedFd};
9 +
10 +use libluna_core::{ButtonState, Event, Modifiers, Point, PointerButton};
11 +use wayland_server::protocol::{wl_keyboard, wl_pointer, wl_seat, wl_touch};
3 12 use wayland_server::{
4 - protocol::{wl_keyboard, wl_pointer, wl_seat, wl_touch},
5 - Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
13 + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
6 14 };
7 15
8 -use crate::state::State;
16 +use crate::state::{DragState, State};
17 +
18 +/// Kompaktowa mapa klawiszy (standardowe reguły XKB z systemowych danych).
19 +const KEYMAP: &str = r#"xkb_keymap {
20 + xkb_keycodes { include "evdev+aliases(qwerty)" };
21 + xkb_types { include "complete" };
22 + xkb_compat { include "complete" };
23 + xkb_symbols { include "pc+us+inet(evdev)" };
24 + xkb_geometry { include "pc(pc105)" };
25 +};
26 +"#;
9 27
10 28 impl GlobalDispatch<wl_seat::WlSeat, ()> for State {
11 29 fn bind(
@@ -17,15 +35,16 @@ impl GlobalDispatch<wl_seat::WlSeat, ()> for State {
17 35 data_init: &mut DataInit<'_, Self>,
18 36 ) {
19 37 let seat = data_init.init(resource, ());
38 + let caps = wl_seat::Capability::Pointer | wl_seat::Capability::Keyboard;
20 39 let _ = seat.send_event(wl_seat::Event::Capabilities {
21 - capabilities: wayland_server::WEnum::Value(wl_seat::Capability::empty()),
40 + capabilities: WEnum::Value(caps),
22 41 });
23 42 }
24 43 }
25 44
26 45 impl Dispatch<wl_seat::WlSeat, ()> for State {
27 46 fn request(
28 - _state: &mut Self,
47 + state: &mut Self,
29 48 _client: &Client,
30 49 _resource: &wl_seat::WlSeat,
31 50 request: wl_seat::Request,
@@ -35,10 +54,18 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
35 54 ) {
36 55 match request {
37 56 wl_seat::Request::GetPointer { id } => {
38 - data_init.init(id, ());
57 + state.pointer = Some(data_init.init(id, ()));
39 58 }
40 59 wl_seat::Request::GetKeyboard { id } => {
41 - data_init.init(id, ());
60 + let kb = data_init.init(id, ());
61 + if let Some((fd, size)) = make_keymap() {
62 + let _ = kb.send_event(wl_keyboard::Event::Keymap {
63 + format: WEnum::Value(wl_keyboard::KeymapFormat::XkbV1),
64 + fd: fd.as_fd(),
65 + size,
66 + });
67 + }
68 + state.keyboard = Some(kb);
42 69 }
43 70 wl_seat::Request::GetTouch { id } => {
44 71 data_init.init(id, ());
@@ -68,3 +95,415 @@ macro_rules! empty_dispatch {
68 95 empty_dispatch!(wl_pointer::WlPointer);
69 96 empty_dispatch!(wl_keyboard::WlKeyboard);
70 97 empty_dispatch!(wl_touch::WlTouch);
98 +
99 +/// Buduje deskryptor z mapą klawiszy (memfd + tekst XKB, zakończony NUL).
100 +fn make_keymap() -> Option<(OwnedFd, u32)> {
101 + let fd = rustix::fs::memfd_create("selene-keymap", rustix::fs::MemfdFlags::CLOEXEC).ok()?;
102 + let mut file = std::fs::File::from(fd);
103 + let mut bytes = KEYMAP.as_bytes().to_vec();
104 + bytes.push(0);
105 + file.write_all(&bytes).ok()?;
106 + file.rewind().ok()?;
107 + Some((OwnedFd::from(file), bytes.len() as u32))
108 +}
109 +
110 +// --- Trasowanie wejścia ---
111 +
112 +/// Trasuje jedno zdarzenie wejścia do klientów.
113 +pub fn handle_input(state: &mut State, event: &Event) {
114 + match event {
115 + Event::PointerMotion { position, .. } => pointer_motion(state, *position),
116 + Event::PointerButton(pe) => pointer_button(state, pe.position, pe.button, pe.state),
117 + Event::PointerLeave => pointer_leave(state),
118 + Event::Scroll(se) => pointer_axis(state, se.dx, se.dy),
119 + Event::Key(ke) => keyboard_key(state, ke.keysym, ke.state),
120 + _ => {}
121 + }
122 +}
123 +
124 +/// Ostatnio przeniesiona pozycja kursora (do trasowania bez `motion`).
125 +fn topmost_window(state: &State, pos: Point) -> Option<usize> {
126 + let windows = state.windows.lock().unwrap();
127 + let mut best: Option<(usize, i32)> = None;
128 + for (i, w) in windows.iter().enumerate() {
129 + if pos.x >= w.x
130 + && pos.x < w.x + w.width as f64
131 + && pos.y >= w.y
132 + && pos.y < w.y + w.height as f64
133 + {
134 + match best {
135 + Some((_, z)) if z >= w.z => {}
136 + _ => best = Some((i, w.z)),
137 + }
138 + }
139 + }
140 + best.map(|(i, _)| i)
141 +}
142 +
143 +fn window_origin(state: &State, slot: usize) -> (f64, f64) {
144 + let windows = state.windows.lock().unwrap();
145 + windows.get(slot).map(|w| (w.x, w.y)).unwrap_or((0.0, 0.0))
146 +}
147 +
148 +fn pointer_motion(state: &mut State, pos: Point) {
149 + // Przeciąganie okna — przesuń je razem z kursorem.
150 + if let Some(drag) = state.drag {
151 + let mut windows = state.windows.lock().unwrap();
152 + if let Some(w) = windows.get_mut(drag.slot) {
153 + w.x = pos.x - drag.off_x;
154 + w.y = pos.y - drag.off_y;
155 + }
156 + }
157 + update_pointer_focus(state, pos);
158 + if let Some((slot, _)) = state.pointer_focus.clone() {
159 + let ptr = state.pointer.clone();
160 + let (ox, oy) = window_origin(state, slot);
161 + let time = state.time_ms();
162 + if let Some(ptr) = ptr {
163 + let _ = ptr.send_event(wl_pointer::Event::Motion {
164 + time,
165 + surface_x: pos.x - ox,
166 + surface_y: pos.y - oy,
167 + });
168 + }
169 + }
170 +}
171 +
172 +fn pointer_button(state: &mut State, pos: Point, button: PointerButton, st: ButtonState) {
173 + update_pointer_focus(state, pos);
174 + if st == ButtonState::Pressed {
175 + if let Some(slot) = topmost_window(state, pos) {
176 + raise_and_focus(state, slot);
177 + }
178 + }
179 + let focused = state.pointer_focus.clone();
180 + let ptr = state.pointer.clone();
181 + if let (Some((slot, _)), Some(ptr)) = (focused, ptr) {
182 + let serial = state.next_serial();
183 + let time = state.time_ms();
184 + let wl_state = match st {
185 + ButtonState::Pressed => WEnum::Value(wl_pointer::ButtonState::Pressed),
186 + ButtonState::Released => WEnum::Value(wl_pointer::ButtonState::Released),
187 + };
188 + let _ = ptr.send_event(wl_pointer::Event::Button {
189 + serial,
190 + time,
191 + button: button_code(button),
192 + state: wl_state,
193 + });
194 + // Lewy przycisk: początek/koniec przeciągania okna.
195 + if button == PointerButton::Left {
196 + match st {
197 + ButtonState::Pressed => {
198 + let (ox, oy) = window_origin(state, slot);
199 + state.drag = Some(DragState {
200 + slot,
201 + off_x: pos.x - ox,
202 + off_y: pos.y - oy,
203 + });
204 + }
205 + ButtonState::Released => state.drag = None,
206 + }
207 + }
208 + }
209 +}
210 +
211 +fn pointer_leave(state: &mut State) {
212 + let ptr = state.pointer.clone();
213 + if let (Some((_, surface)), Some(ptr)) = (state.pointer_focus.clone(), ptr) {
214 + let serial = state.next_serial();
215 + let _ = ptr.send_event(wl_pointer::Event::Leave { serial, surface });
216 + }
217 + state.pointer_focus = None;
218 +}
219 +
220 +fn pointer_axis(state: &mut State, dx: f64, dy: f64) {
221 + let ptr = state.pointer.clone();
222 + if let (true, Some(ptr)) = (state.pointer_focus.is_some(), ptr) {
223 + let time = state.time_ms();
224 + if dy != 0.0 {
225 + let _ = ptr.send_event(wl_pointer::Event::Axis {
226 + time,
227 + axis: WEnum::Value(wl_pointer::Axis::VerticalScroll),
228 + value: dy,
229 + });
230 + }
231 + if dx != 0.0 {
232 + let _ = ptr.send_event(wl_pointer::Event::Axis {
233 + time,
234 + axis: WEnum::Value(wl_pointer::Axis::HorizontalScroll),
235 + value: dx,
236 + });
237 + }
238 + }
239 +}
240 +
241 +fn update_pointer_focus(state: &mut State, pos: Point) {
242 + let target = topmost_window(state, pos);
243 + let same = matches!((state.pointer_focus.as_ref(), target), (Some((s, _)), Some(t)) if *s == t);
244 + if same {
245 + return;
246 + }
247 + // Opuść poprzednią powierzchnię.
248 + let ptr = state.pointer.clone();
249 + if let (Some((_, surface)), Some(ptr)) = (state.pointer_focus.clone(), ptr.clone()) {
250 + let serial = state.next_serial();
251 + let _ = ptr.send_event(wl_pointer::Event::Leave { serial, surface });
252 + }
253 + state.pointer_focus = None;
254 + // Wejdź na nową.
255 + if let Some(slot) = target {
256 + if let Some(surface) = state.slots.get(slot).cloned() {
257 + let serial = state.next_serial();
258 + let (ox, oy) = window_origin(state, slot);
259 + if let Some(ptr) = ptr {
260 + let _ = ptr.send_event(wl_pointer::Event::Enter {
261 + serial,
262 + surface: surface.clone(),
263 + surface_x: pos.x - ox,
264 + surface_y: pos.y - oy,
265 + });
266 + }
267 + state.pointer_focus = Some((slot, surface));
268 + }
269 + }
270 +}
271 +
272 +fn raise_and_focus(state: &mut State, slot: usize) {
273 + let z = state.next_z();
274 + {
275 + let mut windows = state.windows.lock().unwrap();
276 + if let Some(w) = windows.get_mut(slot) {
277 + w.z = z;
278 + }
279 + }
280 + set_keyboard_focus(state, slot);
281 +}
282 +
283 +fn set_keyboard_focus(state: &mut State, slot: usize) {
284 + if state.keyboard_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
285 + return;
286 + }
287 + let kb = state.keyboard.clone();
288 + if let (Some((_, surface)), Some(kb)) = (state.keyboard_focus.clone(), kb.clone()) {
289 + let serial = state.next_serial();
290 + let _ = kb.send_event(wl_keyboard::Event::Leave { serial, surface });
291 + }
292 + state.keyboard_focus = None;
293 + if let Some(surface) = state.slots.get(slot).cloned() {
294 + if let Some(kb) = kb {
295 + let serial = state.next_serial();
296 + let _ = kb.send_event(wl_keyboard::Event::Enter {
297 + serial,
298 + surface: surface.clone(),
299 + keys: vec![],
300 + });
301 + }
302 + state.keyboard_focus = Some((slot, surface));
303 + }
304 +}
305 +
306 +fn keyboard_key(state: &mut State, keysym: u32, st: ButtonState) {
307 + if state.keyboard_focus.is_none() {
308 + return;
309 + }
310 + let kb = state.keyboard.clone();
311 + let Some(kb) = kb else { return };
312 + // Modyfikatory: synchronizuj wciśnięcia Shift/Ctrl/Alt/Super.
313 + sync_modifiers(state, &kb, keysym, st);
314 + if is_modifier_keysym(keysym) {
315 + return;
316 + }
317 + if let Some(key) = keysym_to_keycode(keysym) {
318 + let serial = state.next_serial();
319 + let time = state.time_ms();
320 + let wl_state = match st {
321 + ButtonState::Pressed => WEnum::Value(wl_keyboard::KeyState::Pressed),
322 + ButtonState::Released => WEnum::Value(wl_keyboard::KeyState::Released),
323 + };
324 + let _ = kb.send_event(wl_keyboard::Event::Key {
325 + serial,
326 + time,
327 + key,
328 + state: wl_state,
329 + });
330 + }
331 +}
332 +
333 +/// Wysyła wciśnięcia/zwolnienia klawiszy modyfikujących wynikające z bieżącego stanu.
334 +///
335 +/// `keysym`/`st` opisują zdarzenie, które przyszło; aktualny stan modyfikatorów wyliczamy
336 +/// z samego zdarzenia (nacisk modyfikatora) — dla liter to wystarcza w praktyce.
337 +fn sync_modifiers(state: &mut State, kb: &wl_keyboard::WlKeyboard, keysym: u32, st: ButtonState) {
338 + let mut mods = state.sent_mods;
339 + let target = modifier_for_keysym(keysym);
340 + if let Some(field) = target {
341 + match st {
342 + ButtonState::Pressed => *field(&mut mods) = true,
343 + ButtonState::Released => *field(&mut mods) = false,
344 + }
345 + }
346 + // Wyślij różnice jako wciśnięcia/zwolnienia konkretnych klawiszy.
347 + for (before, now, code) in [
348 + (state.sent_mods.shift, mods.shift, 50u32),
349 + (state.sent_mods.ctrl, mods.ctrl, 37),
350 + (state.sent_mods.alt, mods.alt, 64),
351 + (state.sent_mods.logo, mods.logo, 133),
352 + ] {
353 + if before != now {
354 + let serial = state.next_serial();
355 + let time = state.time_ms();
356 + let ks = if now {
357 + WEnum::Value(wl_keyboard::KeyState::Pressed)
358 + } else {
359 + WEnum::Value(wl_keyboard::KeyState::Released)
360 + };
361 + let _ = kb.send_event(wl_keyboard::Event::Key {
362 + serial,
363 + time,
364 + key: code,
365 + state: ks,
366 + });
367 + }
368 + }
369 + if mods != state.sent_mods {
370 + state.sent_mods = mods;
371 + let serial = state.next_serial();
372 + let _ = kb.send_event(wl_keyboard::Event::Modifiers {
373 + serial,
374 + mods_depressed: modifier_mask(mods),
375 + mods_latched: 0,
376 + mods_locked: 0,
377 + group: 0,
378 + });
379 + }
380 +}
381 +
382 +fn modifier_for_keysym(keysym: u32) -> Option<fn(&mut Modifiers) -> &mut bool> {
383 + match keysym {
384 + 0xffe1 | 0xffe2 => Some(|m| &mut m.shift), // Shift_L / Shift_R
385 + 0xffe3 | 0xffe4 => Some(|m| &mut m.ctrl), // Control_L / Control_R
386 + 0xffe9 | 0xffea => Some(|m| &mut m.alt), // Alt_L / Alt_R
387 + 0xffeb | 0xffec => Some(|m| &mut m.logo), // Super_L / Super_R
388 + _ => None,
389 + }
390 +}
391 +
392 +fn is_modifier_keysym(keysym: u32) -> bool {
393 + modifier_for_keysym(keysym).is_some()
394 +}
395 +
396 +fn modifier_mask(m: Modifiers) -> u32 {
397 + (m.shift as u32) | ((m.ctrl as u32) << 2) | ((m.alt as u32) << 3) | ((m.logo as u32) << 6)
398 +}
399 +
400 +fn button_code(button: PointerButton) -> u32 {
401 + match button {
402 + PointerButton::Left => 0x110,
403 + PointerButton::Right => 0x111,
404 + PointerButton::Middle => 0x112,
405 + PointerButton::Back => 0x116,
406 + PointerButton::Forward => 0x115,
407 + PointerButton::Other(c) => c,
408 + }
409 +}
410 +
411 +/// Keysym → kod klawisza XKB (evdev + 8) dla typowych klawiszy.
412 +fn keysym_to_keycode(keysym: u32) -> Option<u32> {
413 + let code = match keysym {
414 + 0x0020 => 65, // space
415 + 0xff0d => 36, // Return
416 + 0xff1b => 9, // Escape
417 + 0xff08 => 22, // BackSpace
418 + 0xff09 => 23, // Tab
419 + 0xffff => 119, // Delete
420 + 0xff50 => 110, // Home
421 + 0xff57 => 115, // End
422 + 0xff51 => 113, // Left
423 + 0xff52 => 111, // Up
424 + 0xff53 => 114, // Right
425 + 0xff54 => 116, // Down
426 + // litery (małe)
427 + 0x0061 => 38,
428 + 0x0062 => 56,
429 + 0x0063 => 54,
430 + 0x0064 => 40,
431 + 0x0065 => 26,
432 + 0x0066 => 41,
433 + 0x0067 => 42,
434 + 0x0068 => 43,
435 + 0x0069 => 31,
436 + 0x006a => 44,
437 + 0x006b => 45,
438 + 0x006c => 46,
439 + 0x006d => 58,
440 + 0x006e => 57,
441 + 0x006f => 32,
442 + 0x0070 => 33,
443 + 0x0071 => 24,
444 + 0x0072 => 27,
445 + 0x0073 => 39,
446 + 0x0074 => 28,
447 + 0x0075 => 30,
448 + 0x0076 => 55,
449 + 0x0077 => 25,
450 + 0x0078 => 53,
451 + 0x0079 => 29,
452 + 0x007a => 52,
453 + // cyfry
454 + 0x0031 => 10,
455 + 0x0032 => 11,
456 + 0x0033 => 12,
457 + 0x0034 => 13,
458 + 0x0035 => 14,
459 + 0x0036 => 15,
460 + 0x0037 => 16,
461 + 0x0038 => 17,
462 + 0x0039 => 18,
463 + 0x0030 => 19,
464 + // interpunkcja
465 + 0x002d => 20,
466 + 0x003d => 21,
467 + 0x005b => 34,
468 + 0x005d => 35,
469 + 0x003b => 47,
470 + 0x0027 => 48,
471 + 0x0060 => 49,
472 + 0x002c => 59,
473 + 0x002e => 60,
474 + 0x002f => 61,
475 + 0x005c => 51,
476 + _ => return None,
477 + };
478 + Some(code)
479 +}
480 +
481 +#[cfg(test)]
482 +mod tests {
483 + use super::*;
484 +
485 + #[test]
486 + fn maps_common_keys() {
487 + assert_eq!(keysym_to_keycode(0x0061), Some(38)); // a
488 + assert_eq!(keysym_to_keycode(0xff0d), Some(36)); // Enter
489 + assert_eq!(keysym_to_keycode(0xff1b), Some(9)); // Escape
490 + assert_eq!(keysym_to_keycode(0x9999), None);
491 + }
492 +
493 + #[test]
494 + fn modifier_mask_matches_xkb() {
495 + let m = Modifiers {
496 + shift: true,
497 + ctrl: true,
498 + alt: false,
499 + logo: true,
500 + };
501 + assert_eq!(modifier_mask(m), 1 | 4 | 64);
502 + }
503 +
504 + #[test]
505 + fn recognizes_modifier_keysyms() {
506 + assert!(is_modifier_keysym(0xffe1)); // Shift_L
507 + assert!(!is_modifier_keysym(0x0061));
508 + }
509 +}
@@ -3,14 +3,22 @@
3 3 use std::sync::{Arc, Mutex};
4 4 use std::time::Instant;
5 5
6 +use libluna_core::Modifiers;
6 7 use libluna_render::ImageData;
8 +use wayland_server::protocol::wl_keyboard::WlKeyboard;
9 +use wayland_server::protocol::wl_pointer::WlPointer;
10 +use wayland_server::protocol::wl_surface::WlSurface;
7 11
8 -/// Okno (toplevel) w modelu kompozycji: pozycja, rozmiar i gotowe piksele.
12 +use crate::monitors::{OutputInfo, Outputs};
13 +
14 +/// Okno (toplevel) w modelu kompozycji: pozycja, rozmiar, kolejność i gotowe piksele.
9 15 pub struct Window {
10 16 pub x: f64,
11 17 pub y: f64,
12 18 pub width: i32,
13 19 pub height: i32,
20 + /// Kolejność (wyższa = wyżej); do renderu i trafień.
21 + pub z: i32,
14 22 /// Piksele (premultiplied RGBA8) ostatnio zacommitowanego bufora.
15 23 pub data: Arc<ImageData>,
16 24 }
@@ -18,18 +26,43 @@ pub struct Window {
18 26 /// Współdzielony model okien (pisany przez wątek protokołu, czytany przez wyjście).
19 27 pub type WindowModel = Arc<Mutex<Vec<Window>>>;
20 28
29 +/// Trwające przeciąganie okna.
30 +#[derive(Debug, Clone, Copy)]
31 +pub struct DragState {
32 + pub slot: usize,
33 + /// Przesunięcie kursora względem lewego-górnego narożnika okna w chwili wciśnięcia.
34 + pub off_x: f64,
35 + pub off_y: f64,
36 +}
37 +
21 38 /// Stan współdzielony przez wszystkie handlery protokołu.
22 39 pub struct State {
23 40 /// Start pracy kompozytora — do znaczników czasu (frame callbacks).
24 41 pub start: Instant,
25 42 /// Licznik seriali (dla `xdg_surface.configure` itp.).
26 43 serial: u32,
27 - /// Liczniki diagnostyczne (log startowy / debug).
44 + /// Liczniki diagnostyczne.
28 45 pub clients: u32,
29 46 pub surfaces: u32,
30 47 pub toplevels: u32,
31 48 /// Model okien współdzielony z wątkiem wyjścia.
32 49 pub windows: WindowModel,
50 + /// Powierzchnie okien (indeks = slot w [`State::windows`]).
51 + pub slots: Vec<WlSurface>,
52 + /// Licznik kolejności (z-order).
53 + z_counter: i32,
54 + /// Monitory (współdzielone z wątkiem wyjścia).
55 + pub outputs: Outputs,
56 +
57 + // — Wejście —
58 + pub pointer: Option<WlPointer>,
59 + pub keyboard: Option<WlKeyboard>,
60 + pub pointer_focus: Option<(usize, WlSurface)>,
61 + pub keyboard_focus: Option<(usize, WlSurface)>,
62 + /// Modyfikatory, o których już powiadomiliśmy klienta klawiatury.
63 + pub sent_mods: Modifiers,
64 + /// Trwające przeciąganie okna.
65 + pub drag: Option<DragState>,
33 66 }
34 67
35 68 impl Default for State {
@@ -41,6 +74,15 @@ impl Default for State {
41 74 surfaces: 0,
42 75 toplevels: 0,
43 76 windows: Arc::new(Mutex::new(Vec::new())),
77 + slots: Vec::new(),
78 + z_counter: 1,
79 + outputs: Arc::new(Mutex::new(vec![OutputInfo::default()])),
80 + pointer: None,
81 + keyboard: None,
82 + pointer_focus: None,
83 + keyboard_focus: None,
84 + sent_mods: Modifiers::NONE,
85 + drag: None,
44 86 }
45 87 }
46 88 }
@@ -56,6 +98,12 @@ impl State {
56 98 pub fn time_ms(&self) -> u32 {
57 99 self.start.elapsed().as_millis() as u32
58 100 }
101 +
102 + /// Kolejna wartość z-order (podnosi okno na wierzch).
103 + pub fn next_z(&mut self) -> i32 {
104 + self.z_counter += 1;
105 + self.z_counter
106 + }
59 107 }
60 108
61 109 #[cfg(test)]
@@ -72,10 +120,10 @@ mod tests {
72 120 }
73 121
74 122 #[test]
75 - fn time_ms_is_monotonic() {
76 - let s = State::default();
77 - let t0 = s.time_ms();
78 - let t1 = s.time_ms();
79 - assert!(t1 >= t0);
123 + fn z_order_increases() {
124 + let mut s = State::default();
125 + let a = s.next_z();
126 + let b = s.next_z();
127 + assert!(b > a);
80 128 }
81 129 }
@@ -135,10 +135,13 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
135 135 + frame-callbacki, `xdg_surface.configure`, i18n, profil release z LTO.
136 136 ✅ M-K1: **wyjście nested** (`libluna-platform`) + kompozycja okien klientów (`wl_shm` →
137 137 piksele → `libluna-render`); nowy prymityw `Scene::image`/`ImageData` w rendererze.
138 + ✅ M-K2: **wejście** — `wl_output`, `wl_seat` z wskaźnikiem i klawiaturą (`wl_pointer`,
139 + `wl_keyboard` z mapą XKB i syntezą modyfikatorów), magistrala wejścia z wybudzaniem,
140 + oraz **pływające okna** (fokus, podnoszenie, przeciąganie).
138 141 Uruchomienie: `cargo run -p selene` (szczegóły: `de/README.md`).
139 -- Dalej: **wejście** (`libinput`/`evdev` + `wl_pointer`/`wl_keyboard`), zarządzanie oknami/fokusem,
140 - `wl_output`, `xdg_popup`, `layer-shell`, schowek (`wl_data_device`), backend DRM/KMS + GLES,
141 - oraz optymalizacje (cache mapy shm, render przy zmianie modelu).
142 +- Dalej: **wiele monitorów** (DRM/KMS), dekory/zachowanie okien (`xdg-decoration`, resize,
143 + pełny ekran, `xdg_popup`, `layer-shell`), schowek (`wl_data_device`), układy klawiatury/IME,
144 + backend DRM/KMS + GLES, optymalizacje (cache mapy shm, render przy zmianie modelu).
142 145 - **Luna** (`de/luna`) — powłoka środowiska (panel, launcher, OSD) na `libluna-widgets`.
143 146
144 147 ## Później (poza MVP)