← luna

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,