Commit b5ca842
7
files
+375
added
-80
deleted
@@ -36,8 +36,10 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
36
36
przekazuje zdarzenia od rodzica; protokół trasuje je do klientów:
37
37
`wl_pointer` (`enter`/`motion`/`button`/`axis`/`leave`) i `wl_keyboard` (`keymap`
38
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.
39
+- **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.
41
43
- **Monitory**: model `Vec<OutputInfo>` (gotowy na wiele); okno nested = jeden monitor
42
44
(rozmiar śledzony z konfiguracji okna).
43
45
- Wielojęzyczność (`libluna-i18n`) i optymalizacja (`[profile.release]` LTO).
@@ -45,8 +47,8 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
45
47
## Jeszcze nie / Not yet
46
48
47
49
- **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
+- **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`.
50
52
- **Klawiatura**: mapa `us` przez systemowe dane XKB (`/usr/share/X11/xkb`); inne układy,
51
53
IME i `repeat_info` — do zrobienia.
52
54
- Schowek (`wl_data_device`), `wl_output` dynamiczne (zmiana trybu), fokus klawiatury na klik
@@ -9,7 +9,7 @@ use wayland_server::{
9
9
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
10
10
};
11
11
12
-use crate::shell::XdgSurfaceData;
12
+use crate::shell::{XdgSurfaceData, XdgToplevelData};
13
13
use crate::shm::BufferData;
14
14
use crate::state::{State, Window};
15
15
@@ -162,6 +162,15 @@ fn composite(state: &mut State, surface: &wl_surface::WlSurface, data: &SurfaceD
162
162
};
163
163
let image = std::sync::Arc::new(ImageData::new(w as u32, h as u32, rgba));
164
164
let z = state.next_z();
165
+ let title = xdg_surface
166
+ .data::<XdgSurfaceData>()
167
+ .and_then(|d| {
168
+ d.toplevel.lock().unwrap().as_ref().and_then(|tl| {
169
+ tl.data::<XdgToplevelData>()
170
+ .map(|td| td.title.lock().unwrap().clone())
171
+ })
172
+ })
173
+ .unwrap_or_default();
165
174
166
175
let mut slot = data.slot.lock().unwrap();
167
176
let mut windows = state.windows.lock().unwrap();
@@ -176,6 +185,7 @@ fn composite(state: &mut State, surface: &wl_surface::WlSurface, data: &SurfaceD
176
185
width: w,
177
186
height: h,
178
187
z,
188
+ title: title.clone(),
179
189
data: image.clone(),
180
190
});
181
191
*slot = Some(i);
@@ -186,6 +196,9 @@ fn composite(state: &mut State, surface: &wl_surface::WlSurface, data: &SurfaceD
186
196
if let Some(window) = windows.get_mut(idx) {
187
197
window.width = w;
188
198
window.height = h;
199
+ if !title.is_empty() {
200
+ window.title = title;
201
+ }
189
202
window.data = image;
190
203
}
191
204
drop(windows);
@@ -60,9 +60,51 @@ fn draw(frame: &mut Frame, model: &WindowModel) {
60
60
let mut order: Vec<&Window> = windows.iter().collect();
61
61
order.sort_by_key(|w| w.z);
62
62
for w in order {
63
- frame.scene.image(
64
- Rect::from_xywh(w.x, w.y, w.width as f64, w.height as f64),
65
- w.data.clone(),
63
+ // Tło treści (na wypadek przezroczystości bufora klienta).
64
+ frame
65
+ .scene
66
+ .fill_rect(w.content(), Color::from_rgba8(0x00, 0x00, 0x00, 0xff));
67
+ // Treść okna (bufor klienta).
68
+ frame.scene.image(w.content(), w.data.clone());
69
+ // Pasek tytułu (dekoracja po stronie serwera).
70
+ frame
71
+ .scene
72
+ .fill_rect(w.titlebar(), Color::from_rgba8(0x2c, 0x2f, 0x38, 0xff));
73
+ frame.scene.text(
74
+ TextRun::new(Point::new(w.x + 10.0, w.y + 20.0), &w.title, 13.0)
75
+ .with_color(Color::from_rgba8(0xee, 0xee, 0xee, 0xff)),
76
+ );
77
+ // Przycisk zamknięcia (krzyżyk).
78
+ let c = w.close_rect().center();
79
+ let glyph = Color::from_rgba8(0xd0, 0xd0, 0xd0, 0xff);
80
+ frame.scene.stroke_line(
81
+ Point::new(c.x - 4.0, c.y - 4.0),
82
+ Point::new(c.x + 4.0, c.y + 4.0),
83
+ 1.6,
84
+ glyph,
85
+ );
86
+ frame.scene.stroke_line(
87
+ Point::new(c.x + 4.0, c.y - 4.0),
88
+ Point::new(c.x - 4.0, c.y + 4.0),
89
+ 1.6,
90
+ glyph,
91
+ );
92
+ // Uchwyt przeskalowania (3 kropki w narożniku).
93
+ let g = w.grip_rect();
94
+ for i in 0..3 {
95
+ let x = g.right() - 3.0 - i as f64 * 4.0;
96
+ let y = g.bottom() - 3.0 - i as f64 * 4.0;
97
+ frame.scene.fill_rect(
98
+ Rect::from_xywh(x, y, 2.0, 2.0),
99
+ Color::from_rgba8(0x9a, 0x9e, 0xa8, 0xff),
100
+ );
101
+ }
102
+ // Obramowanie całego okna.
103
+ frame.scene.stroke_rounded_rect(
104
+ w.outer(),
105
+ 6.0,
106
+ 1.0,
107
+ Color::from_rgba8(0x50, 0x54, 0x60, 0xff),
66
108
);
67
109
}
68
110
}
@@ -2,18 +2,22 @@
2
2
//!
3
3
//! Zdarzenia wejścia pochodzą z magistrali (`crate::input`) — przekazuje je wątek wyjścia,
4
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.
5
+//! wysyłane do sfokusowanych powierzchni klientów oraz na operacje okien (przeciąganie,
6
+//! przeskalowanie, zamknięcie) wykonywane przez wbudowaną dekorację (SSD).
6
7
7
8
use std::io::{Seek, Write};
8
9
use std::os::fd::{AsFd, OwnedFd};
9
10
10
11
use libluna_core::{ButtonState, Event, Modifiers, Point, PointerButton};
12
+use wayland_protocols::xdg::shell::server::{xdg_surface, xdg_toplevel};
11
13
use wayland_server::protocol::{wl_keyboard, wl_pointer, wl_seat, wl_touch};
12
14
use wayland_server::{
13
15
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
14
16
};
15
17
16
-use crate::state::{DragState, State};
18
+use crate::compositor::SurfaceData;
19
+use crate::shell::XdgSurfaceData;
20
+use crate::state::{DragState, ResizeState, State, TITLEBAR};
17
21
18
22
/// Kompaktowa mapa klawiszy (standardowe reguły XKB z systemowych danych).
19
23
const KEYMAP: &str = r#"xkb_keymap {
@@ -25,6 +29,10 @@ const KEYMAP: &str = r#"xkb_keymap {
25
29
};
26
30
"#;
27
31
32
+/// Minimalny rozmiar treści okna przy przeskalowaniu.
33
+const MIN_W: f64 = 120.0;
34
+const MIN_H: f64 = 80.0;
35
+
28
36
impl GlobalDispatch<wl_seat::WlSeat, ()> for State {
29
37
fn bind(
30
38
_state: &mut Self,
@@ -109,7 +117,16 @@ fn make_keymap() -> Option<(OwnedFd, u32)> {
109
117
110
118
// --- Trasowanie wejścia ---
111
119
112
-/// Trasuje jedno zdarzenie wejścia do klientów.
120
+/// Część okna, w którą trafił wskaźnik.
121
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122
+enum Part {
123
+ Close,
124
+ Grip,
125
+ Title,
126
+ Content,
127
+}
128
+
129
+/// Trasuje jedno zdarzenie wejścia do klientów/okien.
113
130
pub fn handle_input(state: &mut State, event: &Event) {
114
131
match event {
115
132
Event::PointerMotion { position, .. } => pointer_motion(state, *position),
@@ -121,16 +138,38 @@ pub fn handle_input(state: &mut State, event: &Event) {
121
138
}
122
139
}
123
140
124
-/// Ostatnio przeniesiona pozycja kursora (do trasowania bez `motion`).
125
-fn topmost_window(state: &State, pos: Point) -> Option<usize> {
141
+/// Trafienie w okno (najwyższe wg `z`) i w którą jego część.
142
+fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
143
+ let windows = state.windows.lock().unwrap();
144
+ let mut best: Option<(usize, i32)> = None;
145
+ for (i, w) in windows.iter().enumerate() {
146
+ if w.outer().contains(pos) {
147
+ match best {
148
+ Some((_, z)) if z >= w.z => {}
149
+ _ => best = Some((i, w.z)),
150
+ }
151
+ }
152
+ }
153
+ let (i, _) = best?;
154
+ let w = &windows[i];
155
+ let part = if w.close_rect().contains(pos) {
156
+ Part::Close
157
+ } else if w.grip_rect().contains(pos) {
158
+ Part::Grip
159
+ } else if w.titlebar().contains(pos) {
160
+ Part::Title
161
+ } else {
162
+ Part::Content
163
+ };
164
+ Some((i, part))
165
+}
166
+
167
+/// Najwyższe okno, którego **treść** zawiera punkt (do zdarzeń klienta).
168
+fn content_window(state: &State, pos: Point) -> Option<usize> {
126
169
let windows = state.windows.lock().unwrap();
127
170
let mut best: Option<(usize, i32)> = None;
128
171
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
- {
172
+ if w.content().contains(pos) {
134
173
match best {
135
174
Some((_, z)) if z >= w.z => {}
136
175
_ => best = Some((i, w.z)),
@@ -140,13 +179,25 @@ fn topmost_window(state: &State, pos: Point) -> Option<usize> {
140
179
best.map(|(i, _)| i)
141
180
}
142
181
143
-fn window_origin(state: &State, slot: usize) -> (f64, f64) {
182
+fn window_geometry(state: &State, slot: usize) -> Option<(f64, f64, f64, f64)> {
144
183
let windows = state.windows.lock().unwrap();
145
- windows.get(slot).map(|w| (w.x, w.y)).unwrap_or((0.0, 0.0))
184
+ windows
185
+ .get(slot)
186
+ .map(|w| (w.x, w.y, w.width as f64, w.height as f64))
187
+}
188
+
189
+fn content_local(state: &State, slot: usize, pos: Point) -> (f64, f64) {
190
+ match window_geometry(state, slot) {
191
+ Some((x, y, _, _)) => (pos.x - x, pos.y - (y + TITLEBAR)),
192
+ None => (pos.x, pos.y),
193
+ }
146
194
}
147
195
148
196
fn pointer_motion(state: &mut State, pos: Point) {
149
- // Przeciąganie okna — przesuń je razem z kursorem.
197
+ if state.resize.is_some() {
198
+ do_resize(state, pos);
199
+ return;
200
+ }
150
201
if let Some(drag) = state.drag {
151
202
let mut windows = state.windows.lock().unwrap();
152
203
if let Some(w) = windows.get_mut(drag.slot) {
@@ -157,28 +208,74 @@ fn pointer_motion(state: &mut State, pos: Point) {
157
208
update_pointer_focus(state, pos);
158
209
if let Some((slot, _)) = state.pointer_focus.clone() {
159
210
let ptr = state.pointer.clone();
160
- let (ox, oy) = window_origin(state, slot);
211
+ let (lx, ly) = content_local(state, slot, pos);
161
212
let time = state.time_ms();
162
213
if let Some(ptr) = ptr {
163
214
let _ = ptr.send_event(wl_pointer::Event::Motion {
164
215
time,
165
- surface_x: pos.x - ox,
166
- surface_y: pos.y - oy,
216
+ surface_x: lx,
217
+ surface_y: ly,
167
218
});
168
219
}
169
220
}
170
221
}
171
222
172
223
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) {
224
+ if st == ButtonState::Released {
225
+ state.drag = None;
226
+ state.resize = None;
227
+ }
228
+
229
+ // Obsłuż dekorację/okno pod kursorem.
230
+ if let Some((slot, part)) = hit(state, pos) {
231
+ if st == ButtonState::Pressed {
176
232
raise_and_focus(state, slot);
233
+ match part {
234
+ Part::Close => {
235
+ pointer_leave(state);
236
+ send_close(state, slot);
237
+ return;
238
+ }
239
+ Part::Grip => {
240
+ pointer_leave(state);
241
+ if let Some((_, _, w, h)) = window_geometry(state, slot) {
242
+ state.resize = Some(ResizeState {
243
+ slot,
244
+ start_w: w,
245
+ start_h: h,
246
+ start_px: pos.x,
247
+ start_py: pos.y,
248
+ });
249
+ }
250
+ return;
251
+ }
252
+ Part::Title => {
253
+ pointer_leave(state);
254
+ if let Some((ox, oy, _, _)) = window_geometry(state, slot) {
255
+ state.drag = Some(DragState {
256
+ slot,
257
+ off_x: pos.x - ox,
258
+ off_y: pos.y - oy,
259
+ });
260
+ }
261
+ return;
262
+ }
263
+ Part::Content => update_pointer_focus(state, pos),
264
+ }
265
+ } else if part == Part::Content {
266
+ // Zwolnienie nad treścią → przekaż je klientowi poniżej.
267
+ update_pointer_focus(state, pos);
268
+ } else {
269
+ return;
177
270
}
271
+ } else {
272
+ update_pointer_focus(state, pos);
178
273
}
274
+
275
+ // Zdarzenie przycisku do klienta — tylko gdy kursor nad treścią okna.
179
276
let focused = state.pointer_focus.clone();
180
277
let ptr = state.pointer.clone();
181
- if let (Some((slot, _)), Some(ptr)) = (focused, ptr) {
278
+ if let (Some(_), Some(ptr)) = (focused, ptr) {
182
279
let serial = state.next_serial();
183
280
let time = state.time_ms();
184
281
let wl_state = match st {
@@ -191,20 +288,6 @@ fn pointer_button(state: &mut State, pos: Point, button: PointerButton, st: Butt
191
288
button: button_code(button),
192
289
state: wl_state,
193
290
});
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
291
}
209
292
}
210
293
@@ -219,49 +302,49 @@ fn pointer_leave(state: &mut State) {
219
302
220
303
fn pointer_axis(state: &mut State, dx: f64, dy: f64) {
221
304
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
- });
305
+ if state.pointer_focus.is_some() {
306
+ if let Some(ptr) = ptr {
307
+ let time = state.time_ms();
308
+ if dy != 0.0 {
309
+ let _ = ptr.send_event(wl_pointer::Event::Axis {
310
+ time,
311
+ axis: WEnum::Value(wl_pointer::Axis::VerticalScroll),
312
+ value: dy,
313
+ });
314
+ }
315
+ if dx != 0.0 {
316
+ let _ = ptr.send_event(wl_pointer::Event::Axis {
317
+ time,
318
+ axis: WEnum::Value(wl_pointer::Axis::HorizontalScroll),
319
+ value: dx,
320
+ });
321
+ }
237
322
}
238
323
}
239
324
}
240
325
241
326
fn update_pointer_focus(state: &mut State, pos: Point) {
242
- let target = topmost_window(state, pos);
327
+ let target = content_window(state, pos);
243
328
let same = matches!((state.pointer_focus.as_ref(), target), (Some((s, _)), Some(t)) if *s == t);
244
329
if same {
245
330
return;
246
331
}
247
- // Opuść poprzednią powierzchnię.
248
332
let ptr = state.pointer.clone();
249
333
if let (Some((_, surface)), Some(ptr)) = (state.pointer_focus.clone(), ptr.clone()) {
250
334
let serial = state.next_serial();
251
335
let _ = ptr.send_event(wl_pointer::Event::Leave { serial, surface });
252
336
}
253
337
state.pointer_focus = None;
254
- // Wejdź na nową.
255
338
if let Some(slot) = target {
256
339
if let Some(surface) = state.slots.get(slot).cloned() {
257
340
let serial = state.next_serial();
258
- let (ox, oy) = window_origin(state, slot);
341
+ let (lx, ly) = content_local(state, slot, pos);
259
342
if let Some(ptr) = ptr {
260
343
let _ = ptr.send_event(wl_pointer::Event::Enter {
261
344
serial,
262
345
surface: surface.clone(),
263
- surface_x: pos.x - ox,
264
- surface_y: pos.y - oy,
346
+ surface_x: lx,
347
+ surface_y: ly,
265
348
});
266
349
}
267
350
state.pointer_focus = Some((slot, surface));
@@ -269,6 +352,65 @@ fn update_pointer_focus(state: &mut State, pos: Point) {
269
352
}
270
353
}
271
354
355
+/// Przeskalowanie okna w trakcie przeciągania uchwytu; wysyła `xdg_toplevel.configure`.
356
+fn do_resize(state: &mut State, pos: Point) {
357
+ let Some(rs) = state.resize else {
358
+ return;
359
+ };
360
+ let nw = (rs.start_w + (pos.x - rs.start_px)).max(MIN_W);
361
+ let nh = (rs.start_h + (pos.y - rs.start_py)).max(MIN_H);
362
+ {
363
+ let mut windows = state.windows.lock().unwrap();
364
+ if let Some(w) = windows.get_mut(rs.slot) {
365
+ w.width = nw as i32;
366
+ w.height = nh as i32;
367
+ }
368
+ }
369
+ send_configure(state, rs.slot, nw, nh);
370
+}
371
+
372
+/// Wysyła konfigurację rozmiaru (`xdg_surface.configure` + `xdg_toplevel.configure`).
373
+fn send_configure(state: &mut State, slot: usize, w: f64, h: f64) {
374
+ let Some(surface) = state.slots.get(slot).cloned() else {
375
+ return;
376
+ };
377
+ let Some(sd) = surface.data::<SurfaceData>() else {
378
+ return;
379
+ };
380
+ let Some(xs) = sd.xdg_surface.lock().unwrap().clone() else {
381
+ return;
382
+ };
383
+ let serial = state.next_serial();
384
+ let _ = xs.send_event(xdg_surface::Event::Configure { serial });
385
+ if let Some(xd) = xs.data::<XdgSurfaceData>() {
386
+ if let Some(tl) = xd.toplevel.lock().unwrap().as_ref() {
387
+ let _ = tl.send_event(xdg_toplevel::Event::Configure {
388
+ width: w as i32,
389
+ height: h as i32,
390
+ states: vec![3],
391
+ });
392
+ }
393
+ }
394
+}
395
+
396
+/// Prosi klienta o zamknięcie okna (`xdg_toplevel.close`).
397
+fn send_close(state: &mut State, slot: usize) {
398
+ let Some(surface) = state.slots.get(slot).cloned() else {
399
+ return;
400
+ };
401
+ let Some(sd) = surface.data::<SurfaceData>() else {
402
+ return;
403
+ };
404
+ let Some(xs) = sd.xdg_surface.lock().unwrap().clone() else {
405
+ return;
406
+ };
407
+ if let Some(xd) = xs.data::<XdgSurfaceData>() {
408
+ if let Some(tl) = xd.toplevel.lock().unwrap().as_ref() {
409
+ let _ = tl.send_event(xdg_toplevel::Event::Close);
410
+ }
411
+ }
412
+}
413
+
272
414
fn raise_and_focus(state: &mut State, slot: usize) {
273
415
let z = state.next_z();
274
416
{
@@ -309,7 +451,6 @@ fn keyboard_key(state: &mut State, keysym: u32, st: ButtonState) {
309
451
}
310
452
let kb = state.keyboard.clone();
311
453
let Some(kb) = kb else { return };
312
- // Modyfikatory: synchronizuj wciśnięcia Shift/Ctrl/Alt/Super.
313
454
sync_modifiers(state, &kb, keysym, st);
314
455
if is_modifier_keysym(keysym) {
315
456
return;
@@ -331,19 +472,14 @@ fn keyboard_key(state: &mut State, keysym: u32, st: ButtonState) {
331
472
}
332
473
333
474
/// 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
475
fn sync_modifiers(state: &mut State, kb: &wl_keyboard::WlKeyboard, keysym: u32, st: ButtonState) {
338
476
let mut mods = state.sent_mods;
339
- let target = modifier_for_keysym(keysym);
340
- if let Some(field) = target {
477
+ if let Some(field) = modifier_for_keysym(keysym) {
341
478
match st {
342
479
ButtonState::Pressed => *field(&mut mods) = true,
343
480
ButtonState::Released => *field(&mut mods) = false,
344
481
}
345
482
}
346
- // Wyślij różnice jako wciśnięcia/zwolnienia konkretnych klawiszy.
347
483
for (before, now, code) in [
348
484
(state.sent_mods.shift, mods.shift, 50u32),
349
485
(state.sent_mods.ctrl, mods.ctrl, 37),
@@ -481,6 +617,9 @@ fn keysym_to_keycode(keysym: u32) -> Option<u32> {
481
617
#[cfg(test)]
482
618
mod tests {
483
619
use super::*;
620
+ use crate::state::Window;
621
+ use libluna_render::ImageData;
622
+ use std::sync::Arc;
484
623
485
624
#[test]
486
625
fn maps_common_keys() {
@@ -506,4 +645,32 @@ mod tests {
506
645
assert!(is_modifier_keysym(0xffe1)); // Shift_L
507
646
assert!(!is_modifier_keysym(0x0061));
508
647
}
648
+
649
+ fn sample_window() -> Window {
650
+ Window {
651
+ x: 100.0,
652
+ y: 50.0,
653
+ width: 200,
654
+ height: 120,
655
+ z: 1,
656
+ title: "Test".to_owned(),
657
+ data: Arc::new(ImageData::empty(200, 120)),
658
+ }
659
+ }
660
+
661
+ #[test]
662
+ fn decoration_parts_are_laid_out() {
663
+ let w = sample_window();
664
+ // Pasek tytułu na górze, treść pod nim.
665
+ assert_eq!(w.titlebar().bottom(), w.y + TITLEBAR);
666
+ assert_eq!(w.content().y(), w.y + TITLEBAR);
667
+ // Przycisk zamknięcia w prawym górnym narożniku.
668
+ let c = w.close_rect();
669
+ assert!(c.contains(Point::new(w.x + 200.0 - 1.0, w.y + 1.0)));
670
+ assert!(!c.contains(Point::new(w.x + 10.0, w.y + 5.0)));
671
+ // Uchwyt przeskalowania w prawym-dolnym narożniku treści.
672
+ let g = w.grip_rect();
673
+ let c = w.content();
674
+ assert!(g.contains(Point::new(c.right() - 1.0, c.bottom() - 1.0)));
675
+ }
509
676
}
@@ -38,23 +38,20 @@ impl XdgSurfaceData {
38
38
pub struct XdgToplevelData {
39
39
pub title: Mutex<String>,
40
40
pub app_id: Mutex<String>,
41
+ /// Powiązana powierzchnia (do aktualizacji modelu okien).
42
+ pub surface: WlSurface,
41
43
}
42
44
43
45
impl XdgToplevelData {
44
- pub fn new() -> Self {
46
+ pub fn new(surface: WlSurface) -> Self {
45
47
Self {
46
48
title: Mutex::new(String::new()),
47
49
app_id: Mutex::new(String::new()),
50
+ surface,
48
51
}
49
52
}
50
53
}
51
54
52
-impl Default for XdgToplevelData {
53
- fn default() -> Self {
54
- Self::new()
55
- }
56
-}
57
-
58
55
impl GlobalDispatch<xdg_wm_base::XdgWmBase, ()> for State {
59
56
fn bind(
60
57
_state: &mut Self,
@@ -106,7 +103,7 @@ impl Dispatch<xdg_surface::XdgSurface, XdgSurfaceData> for State {
106
103
) {
107
104
match request {
108
105
xdg_surface::Request::GetToplevel { id } => {
109
- let toplevel = data_init.init(id, XdgToplevelData::new());
106
+ let toplevel = data_init.init(id, XdgToplevelData::new(data.wl_surface.clone()));
110
107
*data.toplevel.lock().unwrap() = Some(toplevel);
111
108
}
112
109
xdg_surface::Request::AckConfigure { .. } => {
@@ -129,8 +126,16 @@ impl Dispatch<xdg_toplevel::XdgToplevel, XdgToplevelData> for State {
129
126
) {
130
127
match request {
131
128
xdg_toplevel::Request::SetTitle { title } => {
132
- *data.title.lock().unwrap() = title;
129
+ *data.title.lock().unwrap() = title.clone();
133
130
state.toplevels += 1;
131
+ // Odśwież tytuł w modelu okien (dekoracja rysuje go na pasku).
132
+ if let Some(sd) = data.surface.data::<SurfaceData>() {
133
+ if let Some(slot) = *sd.slot.lock().unwrap() {
134
+ if let Some(window) = state.windows.lock().unwrap().get_mut(slot) {
135
+ window.title = title;
136
+ }
137
+ }
138
+ }
134
139
}
135
140
xdg_toplevel::Request::SetAppId { app_id } => {
136
141
*data.app_id.lock().unwrap() = app_id;
@@ -3,7 +3,7 @@
3
3
use std::sync::{Arc, Mutex};
4
4
use std::time::Instant;
5
5
6
-use libluna_core::Modifiers;
6
+use libluna_core::{Modifiers, Rect};
7
7
use libluna_render::ImageData;
8
8
use wayland_server::protocol::wl_keyboard::WlKeyboard;
9
9
use wayland_server::protocol::wl_pointer::WlPointer;
@@ -11,18 +11,69 @@ use wayland_server::protocol::wl_surface::WlSurface;
11
11
12
12
use crate::monitors::{OutputInfo, Outputs};
13
13
14
+/// Wysokość paska tytułu dekoracji (SSD), w pikselach logicznych.
15
+pub const TITLEBAR: f64 = 30.0;
16
+/// Bok uchwytu przeskalowania w prawym-dolnym narożniku.
17
+pub const GRIP: f64 = 16.0;
18
+
14
19
/// Okno (toplevel) w modelu kompozycji: pozycja, rozmiar, kolejność i gotowe piksele.
15
20
pub struct Window {
16
21
pub x: f64,
17
22
pub y: f64,
23
+ /// Rozmiar **treści** (bufora klienta).
18
24
pub width: i32,
19
25
pub height: i32,
20
26
/// Kolejność (wyższa = wyżej); do renderu i trafień.
21
27
pub z: i32,
28
+ /// Tytuł okna (z `xdg_toplevel.set_title`).
29
+ pub title: String,
22
30
/// Piksele (premultiplied RGBA8) ostatnio zacommitowanego bufora.
23
31
pub data: Arc<ImageData>,
24
32
}
25
33
34
+impl Window {
35
+ /// Prostokąt całego okna (dekoracja + treść).
36
+ pub fn outer(&self) -> Rect {
37
+ Rect::from_xywh(
38
+ self.x,
39
+ self.y,
40
+ self.width as f64,
41
+ TITLEBAR + self.height as f64,
42
+ )
43
+ }
44
+
45
+ /// Pasek tytułu (górna część dekoracji).
46
+ pub fn titlebar(&self) -> Rect {
47
+ Rect::from_xywh(self.x, self.y, self.width as f64, TITLEBAR)
48
+ }
49
+
50
+ /// Obszar treści (klienta).
51
+ pub fn content(&self) -> Rect {
52
+ Rect::from_xywh(
53
+ self.x,
54
+ self.y + TITLEBAR,
55
+ self.width as f64,
56
+ self.height as f64,
57
+ )
58
+ }
59
+
60
+ /// Kwadratowy przycisk zamknięcia w prawym górnym narożniku.
61
+ pub fn close_rect(&self) -> Rect {
62
+ Rect::from_xywh(
63
+ self.x + self.width as f64 - TITLEBAR,
64
+ self.y,
65
+ TITLEBAR,
66
+ TITLEBAR,
67
+ )
68
+ }
69
+
70
+ /// Uchwyt przeskalowania (prawy-dolny narożnik treści).
71
+ pub fn grip_rect(&self) -> Rect {
72
+ let c = self.content();
73
+ Rect::from_xywh(c.right() - GRIP, c.bottom() - GRIP, GRIP, GRIP)
74
+ }
75
+}
76
+
26
77
/// Współdzielony model okien (pisany przez wątek protokołu, czytany przez wyjście).
27
78
pub type WindowModel = Arc<Mutex<Vec<Window>>>;
28
79
@@ -35,6 +86,16 @@ pub struct DragState {
35
86
pub off_y: f64,
36
87
}
37
88
89
+/// Trwające przeskalowanie okna (uchwyt w prawym-dolnym narożniku).
90
+#[derive(Debug, Clone, Copy)]
91
+pub struct ResizeState {
92
+ pub slot: usize,
93
+ pub start_w: f64,
94
+ pub start_h: f64,
95
+ pub start_px: f64,
96
+ pub start_py: f64,
97
+}
98
+
38
99
/// Stan współdzielony przez wszystkie handlery protokołu.
39
100
pub struct State {
40
101
/// Start pracy kompozytora — do znaczników czasu (frame callbacks).
@@ -63,6 +124,8 @@ pub struct State {
63
124
pub sent_mods: Modifiers,
64
125
/// Trwające przeciąganie okna.
65
126
pub drag: Option<DragState>,
127
+ /// Trwające przeskalowanie okna.
128
+ pub resize: Option<ResizeState>,
66
129
}
67
130
68
131
impl Default for State {
@@ -83,6 +146,7 @@ impl Default for State {
83
146
keyboard_focus: None,
84
147
sent_mods: Modifiers::NONE,
85
148
drag: None,
149
+ resize: None,
86
150
}
87
151
}
88
152
}
@@ -138,6 +138,8 @@ 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
143
Uruchomienie: `cargo run -p selene` (szczegóły: `de/README.md`).
142
144
- Dalej: **wiele monitorów** (DRM/KMS), dekory/zachowanie okien (`xdg-decoration`, resize,
143
145
pełny ekran, `xdg_popup`, `layer-shell`), schowek (`wl_data_device`), układy klawiatury/IME,