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)