← luna

Commit 18a9210

8
files
+474
added
-73
deleted
@@ -19,42 +19,46 @@ 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-K2)
22 +## Co już działa / What works (M-K5)
23 23
24 -- Globalne interfejsy: `wl_compositor` (v4), `wl_shm`, **`wl_output`** (v4), `xdg_wm_base` (v1),
25 - **`wl_seat`** (v1) z **wskaźnikiem i klawiaturą**.
24 +- Globalne interfejsy: `wl_compositor` (v4), `wl_shm`, **`wl_output`** (v4, **jeden na monitor**),
25 + `xdg_wm_base` (v1), **`wl_seat`** (v1) z **wskaźnikiem i klawiaturą**, **`zxdg_decoration_manager_v1`**
26 + oraz **`zwlr_layer_shell_v1`**.
26 27 - Cykl życia powierzchni: `wl_surface` (`attach`/`damage`/`frame`/`commit`), `wl_callback`
27 28 (wywołanie `done` przy commicie), `wl_region`.
28 29 - `wl_shm`: `create_pool` → `wl_shm_pool.create_buffer` → `wl_buffer`; piksele bufora są
29 - odczytywane (`mmap` puli, swizzle BGRA→RGBA) i wstawiane do modelu okien.
30 -- xdg-shell: `get_xdg_surface` → `get_toplevel`, pierwszy `commit` wysyła `configure`
30 + odczytywane bez klonowania (cache `mmap` puli, swizzle BGRA→RGBA) i wstawiane do modelu okien.
31 +- xdg-shell: `get_xdg_surface` → `get_toplevel`/`get_popup`, pierwszy `commit` wysyła `configure`
31 32 (oraz `xdg_toplevel.configure`), `ack_configure`, `set_title`/`set_app_id`.
32 33 - **Wyjście nested**: `libluna-platform` otwiera okno na nadrzędnym kompozytorze (osobny
33 34 wątek), a `libluna-render` komponuje scenę (tło + okna w kolejności `z`). Renderer ma
34 - prymityw `Scene::image`/`ImageData`.
35 + prymityw `Scene::image`/`ImageData` (zero-copy).
35 36 - **Wejście**: magistrala (kolejka + para gniazd do wybudzania `poll`). Wątek wyjścia
36 37 przekazuje zdarzenia od rodzica; protokół trasuje je do klientów:
37 38 `wl_pointer` (`enter`/`motion`/`button`/`axis`/`leave`) i `wl_keyboard` (`keymap`
38 39 z wbudowanej mapy XKB, `enter`/`leave`/`key`/`modifiers` z syntezą modyfikatorów).
39 40 - **Zarządzanie oknami (pływające)**: trafienie w okno = fokus + podniesienie (`z`);
40 - **dekoracja po stronie serwera** (SSD): pasek tytułu z tytułem (`set_title`), przycisk
41 - zamknięcia (→ `xdg_toplevel.close`) i uchwyt przeskalowania (→ `xdg_toplevel.configure`);
42 - przeciągnięcie paska przenosi okno, uchwyt zmienia rozmiar; kaskada przy starcie.
43 -- **Monitory**: model `Vec<OutputInfo>` (gotowy na wiele); okno nested = jeden monitor
44 - (rozmiar śledzony z konfiguracji okna).
45 -- Wielojęzyczność (`libluna-i18n`) i optymalizacja (`[profile.release]` LTO).
41 + **dekoracja po stronie serwera** (SSD): pasek tytułu z tytułem (`set_title`), przyciski
42 + **minimalizuj / maksymalizuj / zamknij**, **zmiana rozmiaru za wszystkie krawędzie i narożniki**,
43 + **maksymalizacja** i **pełny ekran** (okno ląduje na swoim monitorze), `xdg-decoration`
44 + (zawsze `server_side`). Skróty: `Super+M` (minimalizuj), `Super+F` (pełny ekran),
45 + `Super+↑`/`Super+↓` (maksymalizuj/przywróć), `Super+Tab` (przełącz okna).
46 +- **`xdg_popup`**: menu/kontekstowe z pozycjonera (`anchor_rect` + offset), zamknięcie
47 + kliknięciem poza (`popup_done`).
48 +- **`wlr-layer-shell`**: panele/docki — kotwice, marginesy, strefa wyłączna, warstwy
49 + (`background`/`bottom`/`top`/`overlay`) i wybór monitora; fundament pulpitu Luna.
50 +- **Monitory**: model `Vec<OutputInfo>` z pozycją i układem, global `wl_output` per monitor,
51 + **przenośne wykrywanie DRM/KMS** (`drm.rs`: ioctl `GETRESOURCES`/`GETCONNECTOR`, tryb natywny),
52 + z fallbackiem na pojedyncze wyjście nested, gdy DRM niedostępne.
53 +- Wielojęzyczność (`libluna-i18n`) i optymalizacja (`[profile.release]` LTO, cache `shm`).
46 54
47 55 ## Jeszcze nie / Not yet
48 56
49 -- **Wiele monitorów**: realne wyjścia DRM/KMS i przypisywanie okien do monitorów (dziś jeden).
50 -- **Dekoracje/zachowanie okien**: brak `xdg-decoration` (mamy SSD własne), resize krawędziami
51 - (dziś uchwyt w narożniku), minimalizacja, pełny ekran, `xdg_popup`, `layer-shell`.
52 -- **Klawiatura**: mapa `us` przez systemowe dane XKB (`/usr/share/X11/xkb`); inne układy,
53 - IME i `repeat_info` — do zrobienia.
54 -- Schowek (`wl_data_device`), `wl_output` dynamiczne (zmiana trybu), fokus klawiatury na klik
55 - poza oknem, SnapEdge’y/układ.
56 -- Optymalizacje: cache mapy `shm`, brak klonowania pikseli na klatkę (cache `ImageData`),
57 - render tylko przy zmianie modelu. Backend sprzętowy (DRM/KMS + GLES).
57 +- **Backend DRM/KMS**: samo **wykrywanie** monitorów działa; skanowanie (bufory, page-flip,
58 + master) i GLES oraz hotplug (`zwlr_output_manager`) — do zrobienia.
59 +- **Schowek** (`wl_data_device`), układy klawiatury inne niż `us`, IME i `repeat_info`.
60 +- `wl_output` dynamiczne (zmiana trybu), fokus klawiatury po kliknięciu poza oknem.
61 +- Render tylko przy zmianie modelu (częściowe odświeżanie/damage).
58 62
59 63 ## Uruchomienie / Running
60 64
@@ -72,13 +76,16 @@ katalog (renderowanie pojawi się, gdy wejdzie backend wyświetlania).
72 76
73 77 ```
74 78 de/selene/src/
75 - main.rs # display, globale, gniazdo, pętla poll (nasłuch + backend + magistrala)
76 - state.rs # stan: model okien, z-order, fokus, drag, monitory
77 - compositor.rs # wl_compositor / wl_surface / wl_region / wl_callback + kompozycja
78 - shm.rs # wl_shm / wl_shm_pool / wl_buffer (+ odczyt pikseli)
79 - shell.rs # xdg_wm_base / xdg_surface / xdg_toplevel / xdg_positioner
80 - monitors.rs # wl_output (model wielu monitorów)
81 - seat.rs # wl_seat: wskaźnik + klawiatura + trasowanie wejścia
79 + main.rs # display, globale (wl_output per monitor), gniazdo, pętla poll
80 + state.rs # stan: model okien (min/max/fullscreen), z-order, fokus, drag/resize, monitory
81 + compositor.rs # wl_compositor / wl_surface / wl_region / wl_callback + kompozycja okien
82 + shm.rs # wl_shm / wl_shm_pool / wl_buffer (+ cache mmap, odczyt pikseli)
83 + shell.rs # xdg_wm_base / xdg_surface / xdg_toplevel / xdg_popup / xdg_positioner
84 + decoration.rs # zxdg_decoration_manager_v1 (SSD: odpowiedź server_side)
85 + layer.rs # zwlr_layer_shell_v1 (panele/docki: kotwice, warstwy, wybór monitora)
86 + monitors.rs # wl_output (model wielu monitorów, układ, trafienia)
87 + drm.rs # wykrywanie monitorów DRM/KMS (ioctl) z fallbackiem nested
88 + seat.rs # wl_seat: wskaźnik + klawiatura + trasowanie wejścia i operacje okien
82 89 input.rs # magistrala wejścia (kolejka + wybudzanie)
83 90 output.rs # wyjście nested (libluna-platform + libluna-render)
84 91 ```
@@ -218,7 +218,8 @@ fn composite_layer(
218 218 let Some(ld) = ls.data::<LayerData>() else {
219 219 return;
220 220 };
221 - let (out_w, out_h) = output_size(state);
221 + let out_idx = ld.output.unwrap_or(0);
222 + let (ox, oy, out_w, out_h) = output_rect(state, out_idx);
222 223 let buffer = data.buffer.lock().unwrap().clone();
223 224 // Pierwszy `commit` (zwykle bez bufora) → wyślij `configure` z rozmiarem.
224 225 if !*ld.configured.lock().unwrap() {
@@ -247,10 +248,22 @@ fn composite_layer(
247 248 };
248 249 let image = std::sync::Arc::new(ImageData::new(w as u32, h as u32, rgba));
249 250 let st = *ld.state.lock().unwrap();
250 - let (x, y) = st.position(out_w, out_h, w as f64, h as f64);
251 + let (rx, ry) = st.position(out_w, out_h, w as f64, h as f64);
251 252 let z = crate::layer::layer_z(st.layer);
252 253 upsert_window(
253 - state, surface, data, x, y, w, h, z, "", false, true, true, image,
254 + state,
255 + surface,
256 + data,
257 + ox + rx,
258 + oy + ry,
259 + w,
260 + h,
261 + z,
262 + "",
263 + false,
264 + true,
265 + true,
266 + image,
254 267 );
255 268 let _ = buffer.send_event(wl_buffer::Event::Release);
256 269 }
@@ -328,15 +341,14 @@ fn upsert_window(
328 341 (idx, created)
329 342 }
330 343
331 -/// Rozmiar monitora (na razie pierwszego; model gotowy na wiele).
332 -fn output_size(state: &State) -> (f64, f64) {
333 - state
334 - .outputs
335 - .lock()
336 - .unwrap()
337 - .first()
338 - .map(|o| (o.width as f64, o.height as f64))
339 - .unwrap_or((1280.0, 800.0))
344 +/// Geometria monitora `idx` (x, y, szerokość, wysokość); `idx` poza zakresem → pierwszy.
345 +fn output_rect(state: &State, idx: usize) -> (f64, f64, f64, f64) {
346 + let outs = state.outputs.lock().unwrap();
347 + let o = outs.get(idx).or_else(|| outs.first());
348 + match o {
349 + Some(o) => (o.x as f64, o.y as f64, o.width as f64, o.height as f64),
350 + None => (0.0, 0.0, 1280.0, 800.0),
351 + }
340 352 }
341 353
342 354 /// Pozycja popupu względem treści rodzica (prostokąt kotwicy + offset).
343 355 new file mode 100644
@@ -0,0 +1,262 @@
1 +//! Wykrywanie monitorów przez DRM/KMS (przenośne — działa na dowolnym Linuksie z `/dev/dri`).
2 +//!
3 +//! To świadomie **szkic**: otwieramy karty DRM, odpytujemy KMS o podłączone konektory
4 +//! i ich tryby, a wynik zwracamy jako model [`OutputInfo`]. Gdy DRM jest niedostępne
5 +//! (np. w środowisku bez uprawnień albo w trybie nested), zwracamy `None` i kompozytor
6 +//! korzysta z pojedynczego wyjścia syntetycznego.
7 +//!
8 +//! Skanowanie (page-flip, alokacja buforów, master) dochodzi później — tutaj chodzi o to,
9 +//! by Selene wiedział, ile i jakich monitorów ma użyć.
10 +
11 +use std::mem::MaybeUninit;
12 +use std::os::fd::{AsFd, BorrowedFd};
13 +use std::path::PathBuf;
14 +
15 +use rustix::fs::{Mode, OFlags};
16 +
17 +use crate::monitors::{layout_horizontal, OutputInfo};
18 +
19 +// — Kody ioctl KMS (`_IOWR`) —
20 +const DRM_IOCTL_MODE_GETRESOURCES: u32 = 0xC040_64A0;
21 +const DRM_IOCTL_MODE_GETCONNECTOR: u32 = 0xC050_64A7;
22 +
23 +const DRM_MODE_CONNECTED: u32 = 1;
24 +/// `DRM_MODE_TYPE_PREFERRED` (bit 3) — natywny tryb monitora.
25 +const DRM_MODE_TYPE_PREFERRED: u32 = 1 << 3;
26 +
27 +/// `struct drm_mode_card_res`.
28 +#[repr(C)]
29 +#[derive(Clone, Copy, Default)]
30 +struct ModeCardRes {
31 + fb_id_ptr: u64,
32 + crtc_id_ptr: u64,
33 + connector_id_ptr: u64,
34 + encoder_id_ptr: u64,
35 + count_fbs: u32,
36 + count_crtcs: u32,
37 + count_connectors: u32,
38 + count_encoders: u32,
39 + min_width: u32,
40 + max_width: u32,
41 + min_height: u32,
42 + max_height: u32,
43 +}
44 +
45 +/// `struct drm_mode_get_connector`.
46 +#[repr(C)]
47 +#[derive(Clone, Copy, Default)]
48 +struct ModeGetConnector {
49 + encoders_ptr: u64,
50 + modes_ptr: u64,
51 + props_ptr: u64,
52 + prop_values_ptr: u64,
53 + count_modes: u32,
54 + count_props: u32,
55 + count_encoders: u32,
56 + encoder_id: u32,
57 + connector_id: u32,
58 + connector_type: u32,
59 + connector_type_id: u32,
60 + connection: u32,
61 + mm_width: u32,
62 + mm_height: u32,
63 + subpixel: u32,
64 + pad: u32,
65 +}
66 +
67 +/// `struct drm_mode_modeinfo`.
68 +#[repr(C)]
69 +#[derive(Clone, Copy, Default)]
70 +struct ModeInfo {
71 + clock: u32,
72 + hdisplay: u16,
73 + hsync_start: u16,
74 + hsync_end: u16,
75 + htotal: u16,
76 + hskew: u16,
77 + vdisplay: u16,
78 + vsync_start: u16,
79 + vsync_end: u16,
80 + vtotal: u16,
81 + vscan: u16,
82 + vrefresh: u32,
83 + flags: u32,
84 + type_: u32,
85 + name: [u8; 32],
86 +}
87 +
88 +/// Opakowanie na ioctl typu „read-write” z jednym struktem wejścia/wyjścia.
89 +struct Rw<T, const OP: u32>(MaybeUninit<T>);
90 +
91 +impl<T, const OP: u32> Rw<T, OP> {
92 + fn new(value: T) -> Self {
93 + Self(MaybeUninit::new(value))
94 + }
95 +}
96 +
97 +unsafe impl<T: Copy, const OP: u32> rustix::ioctl::Ioctl for Rw<T, OP> {
98 + type Output = T;
99 + const OPCODE: rustix::ioctl::Opcode =
100 + rustix::ioctl::Opcode::old(OP as rustix::ioctl::RawOpcode);
101 + const IS_MUTATING: bool = true;
102 +
103 + fn as_ptr(&mut self) -> *mut core::ffi::c_void {
104 + self.0.as_mut_ptr().cast()
105 + }
106 +
107 + unsafe fn output_from_ptr(
108 + _: rustix::ioctl::IoctlOutput,
109 + extract: *mut core::ffi::c_void,
110 + ) -> rustix::io::Result<Self::Output> {
111 + Ok(*(extract as *const T))
112 + }
113 +}
114 +
115 +/// Wykonuje ioctl „read-write” i zwraca zmodyfikowany strukt.
116 +fn ioctl_rw<T: Copy, const OP: u32, F: AsFd>(fd: F, value: T) -> rustix::io::Result<T> {
117 + unsafe { rustix::ioctl::ioctl(fd, Rw::<T, OP>::new(value)) }
118 +}
119 +
120 +/// Wykrywa monitory. `None` oznacza, że DRM/KMS nie jest dostępne (użyj wyjścia nested).
121 +pub fn discover() -> Option<Vec<OutputInfo>> {
122 + let mut cards: Vec<PathBuf> = std::fs::read_dir("/dev/dri")
123 + .ok()?
124 + .filter_map(|e| e.ok())
125 + .map(|e| e.path())
126 + .filter(|p| {
127 + p.file_name()
128 + .map(|n| n.to_string_lossy().starts_with("card"))
129 + .unwrap_or(false)
130 + })
131 + .collect();
132 + cards.sort();
133 +
134 + for card in &cards {
135 + let Ok(fd) = rustix::fs::open(card, OFlags::RDWR | OFlags::CLOEXEC, Mode::empty()) else {
136 + continue;
137 + };
138 + if let Some(mut outs) = probe_card(fd.as_fd()) {
139 + if !outs.is_empty() {
140 + layout_horizontal(&mut outs);
141 + return Some(outs);
142 + }
143 + }
144 + }
145 + None
146 +}
147 +
148 +/// Odpytuje jedną kartę DRM o podłączone konektory.
149 +fn probe_card(fd: BorrowedFd) -> Option<Vec<OutputInfo>> {
150 + // Najpierw liczymy konektory (bez tablic).
151 + let res =
152 + ioctl_rw::<ModeCardRes, DRM_IOCTL_MODE_GETRESOURCES, _>(fd, ModeCardRes::default()).ok()?;
153 + let count = res.count_connectors as usize;
154 + if count == 0 {
155 + return Some(Vec::new());
156 + }
157 +
158 + // Pobieramy identyfikatory konektorów.
159 + let mut ids = vec![0u32; count];
160 + let req = ModeCardRes {
161 + count_connectors: count as u32,
162 + connector_id_ptr: ids.as_mut_ptr() as u64,
163 + ..ModeCardRes::default()
164 + };
165 + ioctl_rw::<ModeCardRes, DRM_IOCTL_MODE_GETRESOURCES, _>(fd, req).ok()?;
166 +
167 + let mut outs = Vec::new();
168 + for id in ids {
169 + if let Some(info) = probe_connector(fd, id) {
170 + outs.push(info);
171 + }
172 + }
173 + Some(outs)
174 +}
175 +
176 +/// Odpytuje jeden konektor: status, tryby, wybiera tryb natywny (lub pierwszy).
177 +fn probe_connector(fd: BorrowedFd, id: u32) -> Option<OutputInfo> {
178 + let c = ioctl_rw::<ModeGetConnector, DRM_IOCTL_MODE_GETCONNECTOR, _>(
179 + fd,
180 + ModeGetConnector {
181 + connector_id: id,
182 + ..ModeGetConnector::default()
183 + },
184 + )
185 + .ok()?;
186 + if c.connection != DRM_MODE_CONNECTED {
187 + return None;
188 + }
189 + let mode_count = c.count_modes as usize;
190 + if mode_count == 0 {
191 + return None;
192 + }
193 +
194 + let mut modes = vec![ModeInfo::default(); mode_count];
195 + let req = ModeGetConnector {
196 + connector_id: id,
197 + count_modes: mode_count as u32,
198 + modes_ptr: modes.as_mut_ptr() as u64,
199 + ..ModeGetConnector::default()
200 + };
201 + let c = ioctl_rw::<ModeGetConnector, DRM_IOCTL_MODE_GETCONNECTOR, _>(fd, req).ok()?;
202 +
203 + let mode = modes
204 + .iter()
205 + .find(|m| m.type_ & DRM_MODE_TYPE_PREFERRED != 0)
206 + .or_else(|| modes.first())?;
207 +
208 + Some(OutputInfo {
209 + name: connector_name(c.connector_type, c.connector_type_id),
210 + width: mode.hdisplay as i32,
211 + height: mode.vdisplay as i32,
212 + scale: 1,
213 + x: 0,
214 + y: 0,
215 + })
216 +}
217 +
218 +/// Nazwa konektora w stylu Wayland (np. `HDMI-A-1`, `eDP-1`).
219 +fn connector_name(kind: u32, index: u32) -> String {
220 + let base = match kind {
221 + 1 => "VGA",
222 + 2 => "DVI-I",
223 + 3 => "DVI-D",
224 + 4 => "DVI-A",
225 + 5 => "Composite",
226 + 6 => "LVDS",
227 + 7 => "Component",
228 + 8 => "DIN",
229 + 9 => "DP",
230 + 10 => "HDMI-A",
231 + 11 => "HDMI-B",
232 + 12 => "TV",
233 + 13 => "eDP",
234 + 14 => "Virtual",
235 + 15 => "DSI",
236 + 16 => "DPI",
237 + 17 => "Writeback",
238 + _ => "Connector",
239 + };
240 + format!("{base}-{index}")
241 +}
242 +
243 +#[cfg(test)]
244 +mod tests {
245 + use super::*;
246 +
247 + #[test]
248 + fn connector_names_follow_wayland_convention() {
249 + assert_eq!(connector_name(13, 1), "eDP-1");
250 + assert_eq!(connector_name(10, 2), "HDMI-A-2");
251 + assert_eq!(connector_name(9, 1), "DP-1");
252 + assert_eq!(connector_name(999, 0), "Connector-0");
253 + }
254 +
255 + #[test]
256 + fn ioctl_struct_sizes_match_kernel_padding() {
257 + // Rozmiary z nagłówków DRM — sprawdzają poprawność `#[repr(C)]` na 64-bit.
258 + assert_eq!(core::mem::size_of::<ModeCardRes>(), 64);
259 + assert_eq!(core::mem::size_of::<ModeGetConnector>(), 80);
260 + assert_eq!(core::mem::size_of::<ModeInfo>(), 68);
261 + }
262 +}
@@ -94,6 +94,8 @@ impl LayerState {
94 94 pub struct LayerData {
95 95 /// Powiązana powierzchnia `wl_surface`.
96 96 pub surface: WlSurface,
97 + /// Monitor, na którym kotwiczymy (indeks w [`crate::monitors::Outputs`]); `None` = pierwszy.
98 + pub output: Option<usize>,
97 99 /// Właściwości (aktualizowane przez `set_*`).
98 100 pub state: Mutex<LayerState>,
99 101 /// Czy wysłano już `configure`.
@@ -101,9 +103,10 @@ pub struct LayerData {
101 103 }
102 104
103 105 impl LayerData {
104 - pub fn new(surface: WlSurface, layer: u32) -> Self {
106 + pub fn new(surface: WlSurface, layer: u32, output: Option<usize>) -> Self {
105 107 Self {
106 108 surface,
109 + output,
107 110 state: Mutex::new(LayerState {
108 111 layer,
109 112 ..LayerState::default()
@@ -154,14 +157,20 @@ impl Dispatch<layer_shell::ZwlrLayerShellV1, ()> for State {
154 157 data_init: &mut DataInit<'_, Self>,
155 158 ) {
156 159 if let layer_shell::Request::GetLayerSurface {
157 - id, surface, layer, ..
160 + id,
161 + surface,
162 + output,
163 + layer,
164 + ..
158 165 } = request
159 166 {
160 167 let layer = match layer {
161 168 WEnum::Value(v) => v as u32,
162 169 WEnum::Unknown(v) => v,
163 170 };
164 - let ls = data_init.init(id, LayerData::new(surface.clone(), layer));
171 + // Indeks monitora niesiony przez zasób `wl_output`.
172 + let output = output.and_then(|o| o.data::<usize>().copied());
173 + let ls = data_init.init(id, LayerData::new(surface.clone(), layer, output));
165 174 // Zapamiętaj powiązanie po stronie `wl_surface` (potrzebne przy `commit`).
166 175 if let Some(sd) = surface.data::<SurfaceData>() {
167 176 *sd.layer.lock().unwrap() = Some(ls);
@@ -7,6 +7,7 @@
7 7
8 8 mod compositor;
9 9 mod decoration;
10 +mod drm;
10 11 mod input;
11 12 mod layer;
12 13 mod monitors;
@@ -37,10 +38,31 @@ fn main() -> Result<(), Box<dyn Error>> {
37 38 let mut display: Display<State> = Display::new()?;
38 39 let mut handle = display.handle();
39 40
41 + let mut state = State::default();
42 +
43 + // Wykrywanie monitorów: DRM/KMS, a gdy niedostępne — pojedyncze wyjście nested.
44 + if let Some(outs) = drm::discover() {
45 + if !outs.is_empty() {
46 + println!(
47 + "{}: {}",
48 + libluna_i18n::tr("Detected outputs"),
49 + outs.iter()
50 + .map(|o| format!("{} {}x{}", o.name, o.width, o.height))
51 + .collect::<Vec<_>>()
52 + .join(", ")
53 + );
54 + *state.outputs.lock().unwrap() = outs;
55 + }
56 + }
57 + let output_count = state.outputs.lock().unwrap().len();
58 +
40 59 // Globalne interfejsy protokołu.
41 60 handle.create_global::<State, wl_compositor::WlCompositor, ()>(4, ());
42 61 handle.create_global::<State, wl_shm::WlShm, ()>(1, ());
43 - handle.create_global::<State, wl_output::WlOutput, ()>(4, ());
62 + // Jeden global `wl_output` na monitor.
63 + for index in 0..output_count {
64 + handle.create_global::<State, wl_output::WlOutput, usize>(4, index);
65 + }
44 66 handle.create_global::<State, xdg_wm_base::XdgWmBase, ()>(1, ());
45 67 handle.create_global::<State, zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, ()>(1, ());
46 68 handle.create_global::<State, zwlr_layer_shell_v1::ZwlrLayerShellV1, ()>(4, ());
@@ -58,8 +80,6 @@ fn main() -> Result<(), Box<dyn Error>> {
58 80 .unwrap_or_default()
59 81 );
60 82
61 - let mut state = State::default();
62 -
63 83 // Magistrala wejścia (wątek wyjścia → wątek protokołu) z parą gniazd do wybudzania.
64 84 let (events, mut wake_read, wake_write) = input::channel();
65 85
@@ -1,7 +1,11 @@
1 -//! `wl_output` — monitory (na razie jeden; model gotowy na wiele).
1 +//! `wl_output` — monitory. Model gotowy na wiele monitorów; wykrywanie w [`crate::drm`].
2 +//!
3 +//! Każdy monitor ma własną pozycję w globalnej przestrzeni współrzędnych, dzięki czemu
4 +//! okna i powierzchnie warstwowe można rozkładać i kotwiczyć na konkretnym wyświetlaczu.
2 5
3 6 use std::sync::{Arc, Mutex};
4 7
8 +use libluna_core::Rect;
5 9 use wayland_server::protocol::wl_output::{self, WlOutput};
6 10 use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
7 11
@@ -14,6 +18,9 @@ pub struct OutputInfo {
14 18 pub width: i32,
15 19 pub height: i32,
16 20 pub scale: i32,
21 + /// Pozycja logiczna (lewy-górny narożnik) w globalnej przestrzeni współrzędnych.
22 + pub x: i32,
23 + pub y: i32,
17 24 }
18 25
19 26 impl Default for OutputInfo {
@@ -23,18 +30,49 @@ impl Default for OutputInfo {
23 30 width: 1280,
24 31 height: 800,
25 32 scale: 1,
33 + x: 0,
34 + y: 0,
26 35 }
27 36 }
28 37 }
29 38
39 +impl OutputInfo {
40 + /// Prostokąt monitora w globalnej przestrzeni współrzędnych.
41 + pub fn rect(&self) -> Rect {
42 + Rect::from_xywh(
43 + self.x as f64,
44 + self.y as f64,
45 + self.width as f64,
46 + self.height as f64,
47 + )
48 + }
49 +}
50 +
30 51 /// Współdzielona lista monitorów.
31 52 pub type Outputs = Arc<Mutex<Vec<OutputInfo>>>;
32 53
54 +/// Układa monitory poziomo, od lewej do prawej (przydatne dla trybu nested i układu lustrzanego).
55 +pub fn layout_horizontal(outs: &mut [OutputInfo]) {
56 + let mut x = 0;
57 + for o in outs.iter_mut() {
58 + o.x = x;
59 + o.y = 0;
60 + x += o.width;
61 + }
62 +}
63 +
64 +/// Indeks monitora zawierającego punkt (lub pierwszy, gdy żaden nie pasuje).
65 +pub fn output_at(outs: &[OutputInfo], x: f64, y: f64) -> usize {
66 + outs.iter()
67 + .position(|o| o.rect().contains(libluna_core::Point::new(x, y)))
68 + .unwrap_or(0)
69 +}
70 +
33 71 /// Wysyła klientowi opis monitora (geometria, tryb, skala, nazwa, gotowe).
34 72 pub fn send_info(output: &WlOutput, info: &OutputInfo) {
35 73 let _ = output.send_event(wl_output::Event::Geometry {
36 - x: 0,
37 - y: 0,
74 + x: info.x,
75 + y: info.y,
38 76 physical_width: info.width * 300 / 1000,
39 77 physical_height: info.height * 300 / 1000,
40 78 subpixel: wayland_server::WEnum::Value(wl_output::Subpixel::Unknown),
@@ -55,36 +93,71 @@ pub fn send_info(output: &WlOutput, info: &OutputInfo) {
55 93 let _ = output.send_event(wl_output::Event::Done);
56 94 }
57 95
58 -impl GlobalDispatch<WlOutput, ()> for State {
96 +impl GlobalDispatch<WlOutput, usize> for State {
59 97 fn bind(
60 98 state: &mut Self,
61 99 _handle: &DisplayHandle,
62 100 _client: &Client,
63 101 resource: New<WlOutput>,
64 - _data: &(),
102 + data: &usize,
65 103 data_init: &mut DataInit<'_, Self>,
66 104 ) {
67 - let output = data_init.init(resource, ());
105 + // Zasób niesie swój indeks monitora (potrzebny m.in. dla `wlr-layer-shell`).
106 + let output = data_init.init(resource, *data);
68 107 let info = state
69 108 .outputs
70 109 .lock()
71 110 .unwrap()
72 - .first()
111 + .get(*data)
73 112 .cloned()
74 113 .unwrap_or_default();
75 114 send_info(&output, &info);
76 115 }
77 116 }
78 117
79 -impl Dispatch<WlOutput, ()> for State {
118 +impl Dispatch<WlOutput, usize> for State {
80 119 fn request(
81 120 _state: &mut Self,
82 121 _client: &Client,
83 122 _resource: &WlOutput,
84 123 _request: wl_output::Request,
85 - _data: &(),
124 + _data: &usize,
86 125 _handle: &DisplayHandle,
87 126 _data_init: &mut DataInit<'_, Self>,
88 127 ) {
89 128 }
90 129 }
130 +
131 +#[cfg(test)]
132 +mod tests {
133 + use super::*;
134 +
135 + fn out(name: &str, w: i32, h: i32) -> OutputInfo {
136 + OutputInfo {
137 + name: name.to_owned(),
138 + width: w,
139 + height: h,
140 + scale: 1,
141 + x: 0,
142 + y: 0,
143 + }
144 + }
145 +
146 + #[test]
147 + fn horizontal_layout_places_monitors_side_by_side() {
148 + let mut outs = vec![out("eDP-1", 1920, 1080), out("HDMI-1", 1280, 720)];
149 + layout_horizontal(&mut outs);
150 + assert_eq!((outs[0].x, outs[0].y), (0, 0));
151 + assert_eq!((outs[1].x, outs[1].y), (1920, 0));
152 + }
153 +
154 + #[test]
155 + fn output_at_finds_the_monitor_under_the_point() {
156 + let mut outs = vec![out("eDP-1", 1920, 1080), out("HDMI-1", 1280, 720)];
157 + layout_horizontal(&mut outs);
158 + assert_eq!(output_at(&outs, 100.0, 100.0), 0);
159 + assert_eq!(output_at(&outs, 2000.0, 100.0), 1);
160 + // Poza wszystkimi monitorami → pierwszy.
161 + assert_eq!(output_at(&outs, -500.0, -500.0), 0);
162 + }
163 +}
@@ -501,15 +501,24 @@ fn window_states(state: &State, slot: usize) -> Vec<u8> {
501 501 states
502 502 }
503 503
504 -/// Rozmiar monitora (na razie pierwszego).
505 -fn output_size(state: &State) -> (f64, f64) {
506 - state
507 - .outputs
504 +/// Prostokąt monitora, na którym leży okno (x, y, szerokość, wysokość).
505 +fn window_output_rect(state: &State, slot: usize) -> (f64, f64, f64, f64) {
506 + let (cx, cy) = state
507 + .windows
508 508 .lock()
509 509 .unwrap()
510 - .first()
511 - .map(|o| (o.width as f64, o.height as f64))
512 - .unwrap_or((1280.0, 800.0))
510 + .get(slot)
511 + .map(|w| {
512 + let c = w.content().center();
513 + (c.x, c.y)
514 + })
515 + .unwrap_or((0.0, 0.0));
516 + let outs = state.outputs.lock().unwrap();
517 + let idx = crate::monitors::output_at(&outs, cx, cy);
518 + match outs.get(idx).or_else(|| outs.first()) {
519 + Some(o) => (o.x as f64, o.y as f64, o.width as f64, o.height as f64),
520 + None => (0.0, 0.0, 1280.0, 800.0),
521 + }
513 522 }
514 523
515 524 /// Przełącza minimalizację (ukrycie przez kompozytor).
@@ -531,7 +540,7 @@ fn toggle_minimize(state: &mut State, slot: usize) {
531 540
532 541 /// Przełącza maksymalizację (zapamiętuje/odtwarza geometrię).
533 542 fn toggle_maximize(state: &mut State, slot: usize) {
534 - let (ow, oh) = output_size(state);
543 + let (ox, oy, ow, oh) = window_output_rect(state, slot);
535 544 {
536 545 let mut windows = state.windows.lock().unwrap();
537 546 let Some(w) = windows.get_mut(slot) else {
@@ -560,8 +569,8 @@ fn toggle_maximize(state: &mut State, slot: usize) {
560 569 w.maximized = true;
561 570 w.fullscreen = false;
562 571 w.minimized = false;
563 - w.x = 0.0;
564 - w.y = 0.0;
572 + w.x = ox;
573 + w.y = oy;
565 574 w.width = ow as i32;
566 575 w.height = (oh - TITLEBAR).max(MIN_H) as i32;
567 576 }
@@ -574,7 +583,7 @@ fn toggle_maximize(state: &mut State, slot: usize) {
574 583
575 584 /// Przełącza pełny ekran (ukrywa dekorację).
576 585 fn toggle_fullscreen(state: &mut State, slot: usize) {
577 - let (ow, oh) = output_size(state);
586 + let (ox, oy, ow, oh) = window_output_rect(state, slot);
578 587 {
579 588 let mut windows = state.windows.lock().unwrap();
580 589 let Some(w) = windows.get_mut(slot) else {
@@ -603,8 +612,8 @@ fn toggle_fullscreen(state: &mut State, slot: usize) {
603 612 w.fullscreen = true;
604 613 w.maximized = false;
605 614 w.minimized = false;
606 - w.x = 0.0;
607 - w.y = 0.0;
615 + w.x = ox;
616 + w.y = oy;
608 617 w.width = ow as i32;
609 618 w.height = oh as i32;
610 619 }
@@ -138,12 +138,21 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
138 138 ✅ M-K2: **wejście** — `wl_output`, `wl_seat` z wskaźnikiem i klawiaturą (`wl_pointer`,
139 139 `wl_keyboard` z mapą XKB i syntezą modyfikatorów), magistrala wejścia z wybudzaniem,
140 140 oraz **pływające okna** (fokus, podnoszenie, przeciąganie).
141 - ✅ M-K3: **dekoracje po stronie serwera** (pasek tytułu, przycisk zamknięcia, uchwyt
142 - przeskalowania) + operacje okna (przeciąganie, `xdg_toplevel.configure`/`close`).
141 + ✅ M-K3: **dekoracje po stronie serwera** (pasek tytułu, przyciski minimalizacji/maksymalizacji/
142 + zamknięcia, uchwyt przeskalowania) + operacje okna (przeciąganie, `xdg_toplevel.configure`/
143 + `close`, **zmiana rozmiaru za wszystkie krawędzie i narożniki**, **maksymalizacja**, **pełny
144 + ekran**, `xdg-decoration` z trybem `server_side`; skróty `Super+M`/`Super+F`/`Super+↑`/
145 + `Super+↓`/`Super+Tab`).
146 + ✅ M-K4: **`xdg_popup`** (menu/kontekstowe z kotwiczeniem do pozycjonera i zamykaniem poza)
147 + oraz **`wlr-layer-shell`** (panele/docki: kotwice, marginesy, strefa wyłączna, warstwy,
148 + wybór monitora) — fundament pulpitu Luna.
149 + ✅ M-K5: **wiele monitorów** — model `OutputInfo` z pozycją, global `wl_output` per monitor,
150 + przenośne **wykrywanie DRM/KMS** (`de/selene/src/drm.rs`, ioctl `GETRESOURCES`/`GETCONNECTOR`)
151 + z fallbackiem na wyjście nested; okna maksymalizują się na swoim monitorze.
152 + ✅ Optymalizacje: cache `mmap` puli `wl_shm` i zero-copy w rendererze (`PixmapRef::from_bytes`).
143 153 Uruchomienie: `cargo run -p selene` (szczegóły: `de/README.md`).
144 -- Dalej: **wiele monitorów** (DRM/KMS), dekory/zachowanie okien (`xdg-decoration`, resize,
145 - pełny ekran, `xdg_popup`, `layer-shell`), schowek (`wl_data_device`), układy klawiatury/IME,
146 - backend DRM/KMS + GLES, optymalizacje (cache mapy shm, render przy zmianie modelu).
154 +- Dalej: schowek (`wl_data_device`), układy klawiatury/IME, realny backend DRM/KMS + GLES
155 + (skanowanie, page-flip, bufory), hotplug monitorów (`wl_output.done`/`zwlr_output_manager`).
147 156 - **Luna** (`de/luna`) — powłoka środowiska (panel, launcher, OSD) na `libluna-widgets`.
148 157
149 158 ## Później (poza MVP)