← luna

Commit 280a111

6
files
+279
added
-14
deleted
@@ -50,13 +50,18 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
50 50 - **Monitory**: model `Vec<OutputInfo>` z pozycją i układem, global `wl_output` per monitor,
51 51 **przenośne wykrywanie DRM/KMS** (`drm.rs`: ioctl `GETRESOURCES`/`GETCONNECTOR`, tryb natywny),
52 52 z fallbackiem na pojedyncze wyjście nested, gdy DRM niedostępne.
53 +- **Schowek** (`wl_data_device_manager`): kopiuj/wklej między klientami — `wl_data_source`,
54 + `data_offer`/`offer`/`selection` rozsyłane do pozostałych urządzeń, `wl_data_offer.receive`
55 + przekazywane do źródła (`.send`).
56 +- **Klawiatura**: układ i wariant z otoczenia (`LUNA_KEYBOARD_LAYOUT`, `LUNA_KEYBOARD_VARIANT`,
57 + sanityzowane), `wl_keyboard.repeat_info`.
53 58 - Wielojęzyczność (`libluna-i18n`) i optymalizacja (`[profile.release]` LTO, cache `shm`).
54 59
55 60 ## Jeszcze nie / Not yet
56 61
57 62 - **Backend DRM/KMS**: samo **wykrywanie** monitorów działa; skanowanie (bufory, page-flip,
58 63 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`.
64 +- **Drag & drop** i **selekcja pierwotna** (poza kopiuj/wklej); IME / `text-input`.
60 65 - `wl_output` dynamiczne (zmiana trybu), fokus klawiatury po kliknięciu poza oknem.
61 66 - Render tylko przy zmianie modelu (częściowe odświeżanie/damage).
62 67
@@ -83,6 +88,7 @@ de/selene/src/
83 88 shell.rs # xdg_wm_base / xdg_surface / xdg_toplevel / xdg_popup / xdg_positioner
84 89 decoration.rs # zxdg_decoration_manager_v1 (SSD: odpowiedź server_side)
85 90 layer.rs # zwlr_layer_shell_v1 (panele/docki: kotwice, warstwy, wybór monitora)
91 + data.rs # wl_data_device_manager (schowek: selekcja, data_offer/source)
86 92 monitors.rs # wl_output (model wielu monitorów, układ, trafienia)
87 93 drm.rs # wykrywanie monitorów DRM/KMS (ioctl) z fallbackiem nested
88 94 seat.rs # wl_seat: wskaźnik + klawiatura + trasowanie wejścia i operacje okien
89 95 new file mode 100644
@@ -0,0 +1,197 @@
1 +//! `wl_data_device_manager` — schowek (selekcja) między klientami.
2 +//!
3 +//! Zakres MVP: **kopiuj/wklej** (selekcja). Drag & drop oraz selekcja pierwotna
4 +//! (`primary selection`) — później.
5 +//!
6 +//! Przepływ: klient-źródło tworzy `wl_data_source`, oferuje typy MIME i wywołuje
7 +//! `wl_data_device.set_selection`. Kompozytor rozsyła pozostałym klientom `wl_data_offer`
8 +//! (`data_offer` → `offer` typów → `selection`). Gdy odbiorca zażąda danych
9 +//! (`wl_data_offer.receive`), przekazujemy żądanie do `wl_data_source.send`.
10 +
11 +use std::os::fd::AsFd;
12 +use std::sync::Mutex;
13 +
14 +use wayland_server::protocol::wl_data_device::{
15 + Event as DeviceEvent, Request as DeviceRequest, WlDataDevice,
16 +};
17 +use wayland_server::protocol::wl_data_device_manager::{
18 + Request as ManagerRequest, WlDataDeviceManager,
19 +};
20 +use wayland_server::protocol::wl_data_offer::{
21 + Event as OfferEvent, Request as OfferRequest, WlDataOffer,
22 +};
23 +use wayland_server::protocol::wl_data_source::{
24 + Event as SourceEvent, Request as SourceRequest, WlDataSource,
25 +};
26 +use wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
27 +
28 +use crate::state::State;
29 +
30 +/// Dane `wl_data_source`: oferowane typy MIME.
31 +#[derive(Default)]
32 +pub struct DataSourceData {
33 + pub mimes: Mutex<Vec<String>>,
34 +}
35 +
36 +/// Dane `wl_data_offer`: źródło, z którego oferta czerpie dane.
37 +pub struct DataOfferData {
38 + pub source: Mutex<Option<WlDataSource>>,
39 +}
40 +
41 +/// Dane `wl_data_device` (na razie puste — urządzenie jest per-seat).
42 +#[derive(Default)]
43 +pub struct DataDeviceData {}
44 +
45 +impl GlobalDispatch<WlDataDeviceManager, ()> for State {
46 + fn bind(
47 + _state: &mut Self,
48 + _handle: &DisplayHandle,
49 + _client: &Client,
50 + resource: New<WlDataDeviceManager>,
51 + _data: &(),
52 + data_init: &mut DataInit<'_, Self>,
53 + ) {
54 + data_init.init(resource, ());
55 + }
56 +}
57 +
58 +impl Dispatch<WlDataDeviceManager, ()> for State {
59 + fn request(
60 + state: &mut Self,
61 + _client: &Client,
62 + _resource: &WlDataDeviceManager,
63 + request: ManagerRequest,
64 + _data: &(),
65 + _handle: &DisplayHandle,
66 + data_init: &mut DataInit<'_, Self>,
67 + ) {
68 + match request {
69 + ManagerRequest::CreateDataSource { id } => {
70 + data_init.init(id, DataSourceData::default());
71 + }
72 + ManagerRequest::GetDataDevice { id, seat: _ } => {
73 + let device = data_init.init(id, DataDeviceData::default());
74 + state.data_devices.push(device);
75 + }
76 + _ => {}
77 + }
78 + }
79 +}
80 +
81 +impl Dispatch<WlDataSource, DataSourceData> for State {
82 + fn request(
83 + state: &mut Self,
84 + _client: &Client,
85 + resource: &WlDataSource,
86 + request: SourceRequest,
87 + data: &DataSourceData,
88 + _handle: &DisplayHandle,
89 + _data_init: &mut DataInit<'_, Self>,
90 + ) {
91 + match request {
92 + SourceRequest::Offer { mime_type } => {
93 + data.mimes.lock().unwrap().push(mime_type);
94 + }
95 + SourceRequest::Destroy => {
96 + // Jeśli to była bieżąca selekcja — wyczyść.
97 + if let Some(cur) = state.selection.as_ref() {
98 + if cur.id() == resource.id() {
99 + state.selection = None;
100 + }
101 + }
102 + }
103 + _ => {}
104 + }
105 + }
106 +}
107 +
108 +impl Dispatch<WlDataDevice, DataDeviceData> for State {
109 + fn request(
110 + state: &mut Self,
111 + _client: &Client,
112 + resource: &WlDataDevice,
113 + request: DeviceRequest,
114 + _data: &DataDeviceData,
115 + handle: &DisplayHandle,
116 + _data_init: &mut DataInit<'_, Self>,
117 + ) {
118 + match request {
119 + DeviceRequest::SetSelection {
120 + source,
121 + serial: _serial,
122 + } => {
123 + state.selection = source.clone();
124 + broadcast_selection(state, handle, resource, source.as_ref());
125 + }
126 + DeviceRequest::Release => {
127 + state.data_devices.retain(|d| d.id() != resource.id());
128 + }
129 + // Drag & drop poza zakresem MVP.
130 + _ => {}
131 + }
132 + }
133 +}
134 +
135 +impl Dispatch<WlDataOffer, DataOfferData> for State {
136 + fn request(
137 + _state: &mut Self,
138 + _client: &Client,
139 + _resource: &WlDataOffer,
140 + request: OfferRequest,
141 + data: &DataOfferData,
142 + _handle: &DisplayHandle,
143 + _data_init: &mut DataInit<'_, Self>,
144 + ) {
145 + if let OfferRequest::Receive { mime_type, fd } = request {
146 + // Przekaż żądanie danych do źródła.
147 + if let Some(src) = data.source.lock().unwrap().clone() {
148 + let _ = src.send_event(SourceEvent::Send {
149 + mime_type,
150 + fd: fd.as_fd(),
151 + });
152 + }
153 + }
154 + }
155 +}
156 +
157 +/// Rozsyła bieżącą selekcję (`data_offer` + `offer` MIME + `selection`) do urządzeń klientów.
158 +fn broadcast_selection(
159 + state: &State,
160 + handle: &DisplayHandle,
161 + requester: &WlDataDevice,
162 + source: Option<&WlDataSource>,
163 +) {
164 + for device in state.data_devices.clone() {
165 + // Pomijamy urządzenie, które ustawiło selekcję (jego klient nie potrzebuje oferty).
166 + if device.id() == requester.id() {
167 + continue;
168 + }
169 + let Some(client) = device.client() else {
170 + continue;
171 + };
172 + match source {
173 + Some(src) => {
174 + let mimes = src
175 + .data::<DataSourceData>()
176 + .map(|d| d.mimes.lock().unwrap().clone())
177 + .unwrap_or_default();
178 + let offer_data = DataOfferData {
179 + source: Mutex::new(Some(src.clone())),
180 + };
181 + let Ok(offer) = client
182 + .create_resource::<WlDataOffer, DataOfferData, State>(handle, 3, offer_data)
183 + else {
184 + continue;
185 + };
186 + let _ = device.send_event(DeviceEvent::DataOffer { id: offer.clone() });
187 + for mime in mimes {
188 + let _ = offer.send_event(OfferEvent::Offer { mime_type: mime });
189 + }
190 + let _ = device.send_event(DeviceEvent::Selection { id: Some(offer) });
191 + }
192 + None => {
193 + let _ = device.send_event(DeviceEvent::Selection { id: None });
194 + }
195 + }
196 + }
197 +}
@@ -6,6 +6,7 @@
6 6 //! idą przez `libluna-platform` + `libluna-render`.
7 7
8 8 mod compositor;
9 +mod data;
9 10 mod decoration;
10 11 mod drm;
11 12 mod input;
@@ -25,7 +26,7 @@ use libluna_core::Event;
25 26 use wayland_protocols::xdg::decoration::zv1::server::zxdg_decoration_manager_v1;
26 27 use wayland_protocols::xdg::shell::server::xdg_wm_base;
27 28 use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_shell_v1;
28 -use wayland_server::protocol::{wl_compositor, wl_output, wl_seat, wl_shm};
29 +use wayland_server::protocol::{wl_compositor, wl_data_device_manager, wl_output, wl_seat, wl_shm};
29 30 use wayland_server::{Display, ListeningSocket};
30 31
31 32 use state::State;
@@ -66,6 +67,7 @@ fn main() -> Result<(), Box<dyn Error>> {
66 67 handle.create_global::<State, xdg_wm_base::XdgWmBase, ()>(1, ());
67 68 handle.create_global::<State, zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, ()>(1, ());
68 69 handle.create_global::<State, zwlr_layer_shell_v1::ZwlrLayerShellV1, ()>(4, ());
70 + handle.create_global::<State, wl_data_device_manager::WlDataDeviceManager, ()>(3, ());
69 71 handle.create_global::<State, wl_seat::WlSeat, ()>(1, ());
70 72
71 73 // Gniazdo nasłuchu: `wayland-0`, `wayland-1`, …
@@ -19,15 +19,40 @@ use crate::compositor::SurfaceData;
19 19 use crate::shell::XdgSurfaceData;
20 20 use crate::state::{DragState, ResizeState, SavedGeom, State, TITLEBAR};
21 21
22 -/// Kompaktowa mapa klawiszy (standardowe reguły XKB z systemowych danych).
23 -const KEYMAP: &str = r#"xkb_keymap {
24 - xkb_keycodes { include "evdev+aliases(qwerty)" };
25 - xkb_types { include "complete" };
26 - xkb_compat { include "complete" };
27 - xkb_symbols { include "pc+us+inet(evdev)" };
28 - xkb_geometry { include "pc(pc105)" };
29 -};
30 -"#;
22 +/// Buduje tekst mapy klawiszy XKB dla danego układu (i opcjonalnego wariantu).
23 +///
24 +/// Nazwy są sanityzowane, aby nie można było wstrzyknąć treści spoza zestawu
25 +/// znaków używanego przez XKB.
26 +fn keymap_text_for(layout: &str, variant: Option<&str>) -> String {
27 + let layout = sanitize_xkb(layout);
28 + let layout = if layout.is_empty() {
29 + "us".to_owned()
30 + } else {
31 + layout
32 + };
33 + let variant = variant.map(sanitize_xkb).filter(|v| !v.is_empty());
34 + let symbols = match variant {
35 + Some(v) => format!("pc+{layout}({v})+inet(evdev)"),
36 + None => format!("pc+{layout}+inet(evdev)"),
37 + };
38 + format!(
39 + "xkb_keymap {{\n xkb_keycodes {{ include \"evdev+aliases(qwerty)\" }};\n xkb_types {{ include \"complete\" }};\n xkb_compat {{ include \"complete\" }};\n xkb_symbols {{ include \"{symbols}\" }};\n xkb_geometry {{ include \"pc(pc105)\" }};\n}};\n"
40 + )
41 +}
42 +
43 +/// Bieżący układ z otoczenia (`LUNA_KEYBOARD_LAYOUT` / `LUNA_KEYBOARD_VARIANT`).
44 +fn keymap_text() -> String {
45 + let layout = std::env::var("LUNA_KEYBOARD_LAYOUT").unwrap_or_else(|_| "us".to_owned());
46 + let variant = std::env::var("LUNA_KEYBOARD_VARIANT").ok();
47 + keymap_text_for(&layout, variant.as_deref())
48 +}
49 +
50 +/// Dopuszcza tylko znaki legalne w nazwie układu XKB.
51 +fn sanitize_xkb(s: &str) -> String {
52 + s.chars()
53 + .filter(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-')
54 + .collect()
55 +}
31 56
32 57 /// Maski krawędzi zmiany rozmiaru.
33 58 const EDGE_TOP: u8 = 1;
@@ -78,6 +103,11 @@ impl Dispatch<wl_seat::WlSeat, ()> for State {
78 103 size,
79 104 });
80 105 }
106 + // Powtarzanie klawiszy: 25/s po 600 ms.
107 + let _ = kb.send_event(wl_keyboard::Event::RepeatInfo {
108 + rate: 25,
109 + delay: 600,
110 + });
81 111 state.keyboard = Some(kb);
82 112 }
83 113 wl_seat::Request::GetTouch { id } => {
@@ -113,7 +143,7 @@ empty_dispatch!(wl_touch::WlTouch);
113 143 fn make_keymap() -> Option<(OwnedFd, u32)> {
114 144 let fd = rustix::fs::memfd_create("selene-keymap", rustix::fs::MemfdFlags::CLOEXEC).ok()?;
115 145 let mut file = std::fs::File::from(fd);
116 - let mut bytes = KEYMAP.as_bytes().to_vec();
146 + let mut bytes = keymap_text().into_bytes();
117 147 bytes.push(0);
118 148 file.write_all(&bytes).ok()?;
119 149 file.rewind().ok()?;
@@ -1047,4 +1077,23 @@ mod tests {
1047 1077 assert_eq!(w.titlebar().bottom(), w.y);
1048 1078 assert_eq!(w.content().y(), w.y);
1049 1079 }
1080 +
1081 + #[test]
1082 + fn keymap_defaults_to_us_layout() {
1083 + let km = keymap_text_for("us", None);
1084 + assert!(km.contains("xkb_keymap"));
1085 + assert!(km.contains("pc+us+inet(evdev)"));
1086 + }
1087 +
1088 + #[test]
1089 + fn keymap_supports_variants_and_sanitizes_input() {
1090 + assert!(keymap_text_for("pl", Some("legacy")).contains("pc+pl(legacy)+inet(evdev)"));
1091 + // Znaki spoza zestawu XKB są odfiltrowane (bezpieczeństwo).
1092 + assert!(keymap_text_for("u; rm -rf", None).contains("pc+urm-rf+inet(evdev)"));
1093 + }
1094 +
1095 + #[test]
1096 + fn empty_layout_falls_back_to_us() {
1097 + assert!(keymap_text_for("!!!", None).contains("pc+us+inet(evdev)"));
1098 + }
1050 1099 }
@@ -5,6 +5,8 @@ use std::time::Instant;
5 5
6 6 use libluna_core::{Modifiers, Point, Rect};
7 7 use libluna_render::ImageData;
8 +use wayland_server::protocol::wl_data_device::WlDataDevice;
9 +use wayland_server::protocol::wl_data_source::WlDataSource;
8 10 use wayland_server::protocol::wl_keyboard::WlKeyboard;
9 11 use wayland_server::protocol::wl_pointer::WlPointer;
10 12 use wayland_server::protocol::wl_surface::WlSurface;
@@ -198,6 +200,10 @@ pub struct State {
198 200 pub keyboard_focus: Option<(usize, WlSurface)>,
199 201 /// Modyfikatory, o których już powiadomiliśmy klienta klawiatury.
200 202 pub sent_mods: Modifiers,
203 + /// Bieżąca selekcja (schowek): źródło danych.
204 + pub selection: Option<WlDataSource>,
205 + /// Urządzenia schowka zarejestrowane przez klientów (do rozsyłania selekcji).
206 + pub data_devices: Vec<WlDataDevice>,
201 207 /// Trwające przeciąganie okna.
202 208 pub drag: Option<DragState>,
203 209 /// Trwające przeskalowanie okna.
@@ -221,6 +227,8 @@ impl Default for State {
221 227 pointer_focus: None,
222 228 keyboard_focus: None,
223 229 sent_mods: Modifiers::NONE,
230 + selection: None,
231 + data_devices: Vec::new(),
224 232 drag: None,
225 233 resize: None,
226 234 }
@@ -149,10 +149,13 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
149 149 ✅ M-K5: **wiele monitorów** — model `OutputInfo` z pozycją, global `wl_output` per monitor,
150 150 przenośne **wykrywanie DRM/KMS** (`de/selene/src/drm.rs`, ioctl `GETRESOURCES`/`GETCONNECTOR`)
151 151 z fallbackiem na wyjście nested; okna maksymalizują się na swoim monitorze.
152 + ✅ M-K6: **schowek** (`wl_data_device_manager`: `data_source`/`data_offer`/`selection`,
153 + kopiuj/wklej między klientami) oraz **układy klawiatury** z otoczenia
154 + (`LUNA_KEYBOARD_LAYOUT`/`LUNA_KEYBOARD_VARIANT`) i `wl_keyboard.repeat_info`.
152 155 ✅ Optymalizacje: cache `mmap` puli `wl_shm` i zero-copy w rendererze (`PixmapRef::from_bytes`).
153 156 Uruchomienie: `cargo run -p selene` (szczegóły: `de/README.md`).
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`).
157 +- Dalej: realny backend DRM/KMS + GLES (skanowanie, page-flip, bufory), drag & drop
158 + i selekcja pierwotna, IME (`text-input`), hotplug monitorów (`zwlr_output_manager`).
156 159 - **Luna** (`de/luna`) — powłoka środowiska (panel, launcher, OSD) na `libluna-widgets`.
157 160
158 161 ## Później (poza MVP)