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)