← luna

Commit 57194f0

8
files
+524
added
-68
deleted
@@ -18,6 +18,6 @@ libluna-i18n.workspace = true
18 18 libluna-platform.workspace = true
19 19
20 20 wayland-server = "0.31"
21 -wayland-protocols = { version = "0.32", features = ["server"] }
21 +wayland-protocols = { version = "0.32", features = ["server", "unstable"] }
22 22 wayland-protocols-wlr = { version = "0.3", features = ["server"] }
23 23 rustix = { version = "0.38", features = ["event", "fs", "time", "mm"] }
@@ -308,6 +308,10 @@ fn upsert_window(
308 308 popup,
309 309 layer,
310 310 mapped: true,
311 + minimized: false,
312 + maximized: false,
313 + fullscreen: false,
314 + saved: None,
311 315 data: image,
312 316 });
313 317 *slot = Some(i);
@@ -406,7 +410,7 @@ fn maybe_configure_xdg(state: &mut State, data: &SurfaceData) {
406 410 let _ = toplevel.send_event(xdg_toplevel::Event::Configure {
407 411 width: 1024,
408 412 height: 720,
409 - states: vec![3], // ACTIVATED
413 + states: vec![4], // ACTIVATED
410 414 });
411 415 let _ = toplevel.send_event(xdg_toplevel::Event::WmCapabilities {
412 416 capabilities: vec![],
413 417 new file mode 100644
@@ -0,0 +1,73 @@
1 +//! `zxdg_decoration_manager_v1` — negocjacja dekoracji okien.
2 +//!
3 +//! Selene zawsze rysuje dekoracje po stronie serwera (SSD), więc klientom
4 +//! korzystającym z tego protokołu odpowiadamy trybem `server_side`.
5 +
6 +use wayland_protocols::xdg::decoration::zv1::server::{
7 + zxdg_decoration_manager_v1 as decoration_manager,
8 + zxdg_toplevel_decoration_v1 as toplevel_decoration,
9 +};
10 +use wayland_server::{
11 + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
12 +};
13 +
14 +use crate::shell::XdgToplevelData;
15 +use crate::state::State;
16 +
17 +/// Uchwyt obiektu dekoracji okna.
18 +pub type ToplevelDecoration = toplevel_decoration::ZxdgToplevelDecorationV1;
19 +
20 +impl GlobalDispatch<decoration_manager::ZxdgDecorationManagerV1, ()> for State {
21 + fn bind(
22 + _state: &mut Self,
23 + _handle: &DisplayHandle,
24 + _client: &Client,
25 + resource: New<decoration_manager::ZxdgDecorationManagerV1>,
26 + _data: &(),
27 + data_init: &mut DataInit<'_, Self>,
28 + ) {
29 + data_init.init(resource, ());
30 + }
31 +}
32 +
33 +impl Dispatch<decoration_manager::ZxdgDecorationManagerV1, ()> for State {
34 + fn request(
35 + _state: &mut Self,
36 + _client: &Client,
37 + _resource: &decoration_manager::ZxdgDecorationManagerV1,
38 + request: decoration_manager::Request,
39 + _data: &(),
40 + _handle: &DisplayHandle,
41 + data_init: &mut DataInit<'_, Self>,
42 + ) {
43 + if let decoration_manager::Request::GetToplevelDecoration { id, toplevel } = request {
44 + let deco = data_init.init(id, ());
45 + if let Some(td) = toplevel.data::<XdgToplevelData>() {
46 + *td.decoration.lock().unwrap() = Some(deco);
47 + }
48 + }
49 + }
50 +}
51 +
52 +impl Dispatch<toplevel_decoration::ZxdgToplevelDecorationV1, ()> for State {
53 + fn request(
54 + _state: &mut Self,
55 + _client: &Client,
56 + resource: &toplevel_decoration::ZxdgToplevelDecorationV1,
57 + request: toplevel_decoration::Request,
58 + _data: &(),
59 + _handle: &DisplayHandle,
60 + _data_init: &mut DataInit<'_, Self>,
61 + ) {
62 + match request {
63 + // Klient prosi o tryb — my i tak dekorujemy po stronie serwera.
64 + toplevel_decoration::Request::SetMode { .. }
65 + | toplevel_decoration::Request::UnsetMode => {
66 + let _ = resource.send_event(toplevel_decoration::Event::Configure {
67 + mode: WEnum::Value(toplevel_decoration::Mode::ServerSide),
68 + });
69 + }
70 + _ => {}
71 + }
72 + }
73 +}
@@ -6,6 +6,7 @@
6 6 //! idą przez `libluna-platform` + `libluna-render`.
7 7
8 8 mod compositor;
9 +mod decoration;
9 10 mod input;
10 11 mod layer;
11 12 mod monitors;
@@ -20,6 +21,7 @@ use std::os::fd::{AsRawFd, BorrowedFd};
20 21 use std::sync::Arc;
21 22
22 23 use libluna_core::Event;
24 +use wayland_protocols::xdg::decoration::zv1::server::zxdg_decoration_manager_v1;
23 25 use wayland_protocols::xdg::shell::server::xdg_wm_base;
24 26 use wayland_protocols_wlr::layer_shell::v1::server::zwlr_layer_shell_v1;
25 27 use wayland_server::protocol::{wl_compositor, wl_output, wl_seat, wl_shm};
@@ -40,6 +42,7 @@ fn main() -> Result<(), Box<dyn Error>> {
40 42 handle.create_global::<State, wl_shm::WlShm, ()>(1, ());
41 43 handle.create_global::<State, wl_output::WlOutput, ()>(4, ());
42 44 handle.create_global::<State, xdg_wm_base::XdgWmBase, ()>(1, ());
45 + handle.create_global::<State, zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, ()>(1, ());
43 46 handle.create_global::<State, zwlr_layer_shell_v1::ZwlrLayerShellV1, ()>(4, ());
44 47 handle.create_global::<State, wl_seat::WlSeat, ()>(1, ());
45 48
@@ -78,9 +78,9 @@ fn draw(frame: &mut Frame, model: &WindowModel) {
78 78 TextRun::new(Point::new(w.x + 10.0, w.y + 20.0), &w.title, 13.0)
79 79 .with_color(Color::from_rgba8(0xee, 0xee, 0xee, 0xff)),
80 80 );
81 - // Przycisk zamknięcia (krzyżyk).
82 - let c = w.close_rect().center();
83 81 let glyph = Color::from_rgba8(0xd0, 0xd0, 0xd0, 0xff);
82 + // Zamknięcie — krzyżyk.
83 + let c = w.close_rect().center();
84 84 frame.scene.stroke_line(
85 85 Point::new(c.x - 4.0, c.y - 4.0),
86 86 Point::new(c.x + 4.0, c.y + 4.0),
@@ -93,16 +93,22 @@ fn draw(frame: &mut Frame, model: &WindowModel) {
93 93 1.6,
94 94 glyph,
95 95 );
96 - // Uchwyt przeskalowania (3 kropki w narożniku).
97 - let g = w.grip_rect();
98 - for i in 0..3 {
99 - let x = g.right() - 3.0 - i as f64 * 4.0;
100 - let y = g.bottom() - 3.0 - i as f64 * 4.0;
101 - frame.scene.fill_rect(
102 - Rect::from_xywh(x, y, 2.0, 2.0),
103 - Color::from_rgba8(0x9a, 0x9e, 0xa8, 0xff),
104 - );
105 - }
96 + // Maksymalizacja — kwadrat.
97 + let m = w.maximize_rect().center();
98 + frame.scene.stroke_rounded_rect(
99 + Rect::from_xywh(m.x - 4.0, m.y - 4.0, 8.0, 8.0),
100 + 0.0,
101 + 1.4,
102 + glyph,
103 + );
104 + // Minimalizacja — kreska.
105 + let n = w.minimize_rect().center();
106 + frame.scene.stroke_line(
107 + Point::new(n.x - 4.0, n.y + 3.0),
108 + Point::new(n.x + 4.0, n.y + 3.0),
109 + 1.6,
110 + glyph,
111 + );
106 112 }
107 113 // Obramowanie całego okna (tylko zwykłe okna).
108 114 if !w.layer {
@@ -17,7 +17,7 @@ use wayland_server::{
17 17
18 18 use crate::compositor::SurfaceData;
19 19 use crate::shell::XdgSurfaceData;
20 -use crate::state::{DragState, ResizeState, State, TITLEBAR};
20 +use crate::state::{DragState, ResizeState, SavedGeom, State, TITLEBAR};
21 21
22 22 /// Kompaktowa mapa klawiszy (standardowe reguły XKB z systemowych danych).
23 23 const KEYMAP: &str = r#"xkb_keymap {
@@ -29,6 +29,11 @@ const KEYMAP: &str = r#"xkb_keymap {
29 29 };
30 30 "#;
31 31
32 +/// Maski krawędzi zmiany rozmiaru.
33 +const EDGE_TOP: u8 = 1;
34 +const EDGE_BOTTOM: u8 = 2;
35 +const EDGE_LEFT: u8 = 4;
36 +const EDGE_RIGHT: u8 = 8;
32 37 /// Minimalny rozmiar treści okna przy przeskalowaniu.
33 38 const MIN_W: f64 = 120.0;
34 39 const MIN_H: f64 = 80.0;
@@ -121,9 +126,12 @@ fn make_keymap() -> Option<(OwnedFd, u32)> {
121 126 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
122 127 enum Part {
123 128 Close,
124 - Grip,
129 + Maximize,
130 + Minimize,
125 131 Title,
126 132 Content,
133 + /// Zmiana rozmiaru za krawędź/narożnik (bitmaska: 1=góra,2=dół,4=lewo,8=prawo).
134 + Resize(u8),
127 135 }
128 136
129 137 /// Trasuje jedno zdarzenie wejścia do klientów/okien.
@@ -143,7 +151,7 @@ fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
143 151 let windows = state.windows.lock().unwrap();
144 152 let mut best: Option<(usize, i32)> = None;
145 153 for (i, w) in windows.iter().enumerate() {
146 - if w.mapped && w.outer().contains(pos) {
154 + if w.mapped && !w.minimized && w.outer().contains(pos) {
147 155 match best {
148 156 Some((_, z)) if z >= w.z => {}
149 157 _ => best = Some((i, w.z)),
@@ -154,12 +162,19 @@ fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
154 162 let w = &windows[i];
155 163 let part = if w.close_rect().contains(pos) {
156 164 Part::Close
157 - } else if w.grip_rect().contains(pos) {
158 - Part::Grip
159 - } else if w.titlebar().contains(pos) {
160 - Part::Title
165 + } else if w.maximize_rect().contains(pos) {
166 + Part::Maximize
167 + } else if w.minimize_rect().contains(pos) {
168 + Part::Minimize
161 169 } else {
162 - Part::Content
170 + let edges = w.resize_edges(pos);
171 + if edges != 0 {
172 + Part::Resize(edges)
173 + } else if w.titlebar().contains(pos) {
174 + Part::Title
175 + } else {
176 + Part::Content
177 + }
163 178 };
164 179 Some((i, part))
165 180 }
@@ -169,7 +184,7 @@ fn content_window(state: &State, pos: Point) -> Option<usize> {
169 184 let windows = state.windows.lock().unwrap();
170 185 let mut best: Option<(usize, i32)> = None;
171 186 for (i, w) in windows.iter().enumerate() {
172 - if w.mapped && w.content().contains(pos) {
187 + if w.mapped && !w.minimized && w.content().contains(pos) {
173 188 match best {
174 189 Some((_, z)) if z >= w.z => {}
175 190 _ => best = Some((i, w.z)),
@@ -283,17 +298,14 @@ fn pointer_button(state: &mut State, pos: Point, button: PointerButton, st: Butt
283 298 send_close(state, slot);
284 299 return;
285 300 }
286 - Part::Grip => {
301 + Part::Minimize => {
287 302 pointer_leave(state);
288 - if let Some((_, _, w, h)) = window_geometry(state, slot) {
289 - state.resize = Some(ResizeState {
290 - slot,
291 - start_w: w,
292 - start_h: h,
293 - start_px: pos.x,
294 - start_py: pos.y,
295 - });
296 - }
303 + toggle_minimize(state, slot);
304 + return;
305 + }
306 + Part::Maximize => {
307 + pointer_leave(state);
308 + toggle_maximize(state, slot);
297 309 return;
298 310 }
299 311 Part::Title => {
@@ -307,6 +319,22 @@ fn pointer_button(state: &mut State, pos: Point, button: PointerButton, st: Butt
307 319 }
308 320 return;
309 321 }
322 + Part::Resize(edges) => {
323 + pointer_leave(state);
324 + if let Some((x, y, w, h)) = window_geometry(state, slot) {
325 + state.resize = Some(ResizeState {
326 + slot,
327 + edges,
328 + start_x: x,
329 + start_y: y,
330 + start_w: w,
331 + start_h: h,
332 + start_px: pos.x,
333 + start_py: pos.y,
334 + });
335 + }
336 + return;
337 + }
310 338 Part::Content => update_pointer_focus(state, pos),
311 339 }
312 340 } else if part == Part::Content {
@@ -399,24 +427,42 @@ fn update_pointer_focus(state: &mut State, pos: Point) {
399 427 }
400 428 }
401 429
402 -/// Przeskalowanie okna w trakcie przeciągania uchwytu; wysyła `xdg_toplevel.configure`.
430 +/// Przeskalowanie okna w trakcie przeciągania krawędzi/narożnika; wysyła `xdg_toplevel.configure`.
403 431 fn do_resize(state: &mut State, pos: Point) {
404 432 let Some(rs) = state.resize else {
405 433 return;
406 434 };
407 - let nw = (rs.start_w + (pos.x - rs.start_px)).max(MIN_W);
408 - let nh = (rs.start_h + (pos.y - rs.start_py)).max(MIN_H);
435 + let dx = pos.x - rs.start_px;
436 + let dy = pos.y - rs.start_py;
437 + let mut x = rs.start_x;
438 + let mut y = rs.start_y;
439 + let mut w = rs.start_w;
440 + let mut h = rs.start_h;
441 + if rs.edges & EDGE_LEFT != 0 {
442 + w = (rs.start_w - dx).max(MIN_W);
443 + x = rs.start_x + (rs.start_w - w);
444 + } else if rs.edges & EDGE_RIGHT != 0 {
445 + w = (rs.start_w + dx).max(MIN_W);
446 + }
447 + if rs.edges & EDGE_TOP != 0 {
448 + h = (rs.start_h - dy).max(MIN_H);
449 + y = rs.start_y + (rs.start_h - h);
450 + } else if rs.edges & EDGE_BOTTOM != 0 {
451 + h = (rs.start_h + dy).max(MIN_H);
452 + }
409 453 {
410 454 let mut windows = state.windows.lock().unwrap();
411 - if let Some(w) = windows.get_mut(rs.slot) {
412 - w.width = nw as i32;
413 - w.height = nh as i32;
455 + if let Some(w2) = windows.get_mut(rs.slot) {
456 + w2.x = x;
457 + w2.y = y;
458 + w2.width = w as i32;
459 + w2.height = h as i32;
414 460 }
415 461 }
416 - send_configure(state, rs.slot, nw, nh);
462 + send_configure(state, rs.slot, w, h);
417 463 }
418 464
419 -/// Wysyła konfigurację rozmiaru (`xdg_surface.configure` + `xdg_toplevel.configure`).
465 +/// Wysyła konfigurację rozmiaru i stanów (`xdg_surface.configure` + `xdg_toplevel.configure`).
420 466 fn send_configure(state: &mut State, slot: usize, w: f64, h: f64) {
421 467 let Some(surface) = state.slots.get(slot).cloned() else {
422 468 return;
@@ -427,6 +473,7 @@ fn send_configure(state: &mut State, slot: usize, w: f64, h: f64) {
427 473 let Some(xs) = sd.xdg_surface.lock().unwrap().clone() else {
428 474 return;
429 475 };
476 + let states = window_states(state, slot);
430 477 let serial = state.next_serial();
431 478 let _ = xs.send_event(xdg_surface::Event::Configure { serial });
432 479 if let Some(xd) = xs.data::<XdgSurfaceData>() {
@@ -434,12 +481,155 @@ fn send_configure(state: &mut State, slot: usize, w: f64, h: f64) {
434 481 let _ = tl.send_event(xdg_toplevel::Event::Configure {
435 482 width: w as i32,
436 483 height: h as i32,
437 - states: vec![3],
484 + states,
438 485 });
439 486 }
440 487 }
441 488 }
442 489
490 +/// Stany `xdg_toplevel` dla okna (ACTIVATED + ewentualnie MAXIMIZED/FULLSCREEN).
491 +fn window_states(state: &State, slot: usize) -> Vec<u8> {
492 + let mut states = vec![4u8]; // ACTIVATED
493 + if let Some(w) = state.windows.lock().unwrap().get(slot) {
494 + if w.maximized {
495 + states.push(1); // MAXIMIZED
496 + }
497 + if w.fullscreen {
498 + states.push(2); // FULLSCREEN
499 + }
500 + }
501 + states
502 +}
503 +
504 +/// Rozmiar monitora (na razie pierwszego).
505 +fn output_size(state: &State) -> (f64, f64) {
506 + state
507 + .outputs
508 + .lock()
509 + .unwrap()
510 + .first()
511 + .map(|o| (o.width as f64, o.height as f64))
512 + .unwrap_or((1280.0, 800.0))
513 +}
514 +
515 +/// Przełącza minimalizację (ukrycie przez kompozytor).
516 +fn toggle_minimize(state: &mut State, slot: usize) {
517 + let minimized = {
518 + let mut windows = state.windows.lock().unwrap();
519 + match windows.get_mut(slot) {
520 + Some(w) if !w.popup && !w.layer => {
521 + w.minimized = !w.minimized;
522 + w.minimized
523 + }
524 + _ => return,
525 + }
526 + };
527 + if minimized {
528 + clear_focus_if(state, slot);
529 + }
530 +}
531 +
532 +/// Przełącza maksymalizację (zapamiętuje/odtwarza geometrię).
533 +fn toggle_maximize(state: &mut State, slot: usize) {
534 + let (ow, oh) = output_size(state);
535 + {
536 + let mut windows = state.windows.lock().unwrap();
537 + let Some(w) = windows.get_mut(slot) else {
538 + return;
539 + };
540 + if w.popup || w.layer {
541 + return;
542 + }
543 + if w.maximized {
544 + if let Some(sv) = w.saved.take() {
545 + w.x = sv.x;
546 + w.y = sv.y;
547 + w.width = sv.width;
548 + w.height = sv.height;
549 + }
550 + w.maximized = false;
551 + } else {
552 + if !w.fullscreen {
553 + w.saved = Some(SavedGeom {
554 + x: w.x,
555 + y: w.y,
556 + width: w.width,
557 + height: w.height,
558 + });
559 + }
560 + w.maximized = true;
561 + w.fullscreen = false;
562 + w.minimized = false;
563 + w.x = 0.0;
564 + w.y = 0.0;
565 + w.width = ow as i32;
566 + w.height = (oh - TITLEBAR).max(MIN_H) as i32;
567 + }
568 + }
569 + let (nw, nh) = window_geometry(state, slot)
570 + .map(|(_, _, w, h)| (w, h))
571 + .unwrap_or((ow, oh));
572 + send_configure(state, slot, nw, nh);
573 +}
574 +
575 +/// Przełącza pełny ekran (ukrywa dekorację).
576 +fn toggle_fullscreen(state: &mut State, slot: usize) {
577 + let (ow, oh) = output_size(state);
578 + {
579 + let mut windows = state.windows.lock().unwrap();
580 + let Some(w) = windows.get_mut(slot) else {
581 + return;
582 + };
583 + if w.popup || w.layer {
584 + return;
585 + }
586 + if w.fullscreen {
587 + if let Some(sv) = w.saved.take() {
588 + w.x = sv.x;
589 + w.y = sv.y;
590 + w.width = sv.width;
591 + w.height = sv.height;
592 + }
593 + w.fullscreen = false;
594 + } else {
595 + if !w.maximized {
596 + w.saved = Some(SavedGeom {
597 + x: w.x,
598 + y: w.y,
599 + width: w.width,
600 + height: w.height,
601 + });
602 + }
603 + w.fullscreen = true;
604 + w.maximized = false;
605 + w.minimized = false;
606 + w.x = 0.0;
607 + w.y = 0.0;
608 + w.width = ow as i32;
609 + w.height = oh as i32;
610 + }
611 + }
612 + let (nw, nh) = window_geometry(state, slot)
613 + .map(|(_, _, w, h)| (w, h))
614 + .unwrap_or((ow, oh));
615 + send_configure(state, slot, nw, nh);
616 +}
617 +
618 +/// Zdejmuje fokus klawiatury/wskaźnika z okna (np. po minimalizacji).
619 +fn clear_focus_if(state: &mut State, slot: usize) {
620 + if state.keyboard_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
621 + let kb = state.keyboard.clone();
622 + if let (Some((_, surface)), Some(kb)) = (state.keyboard_focus.clone(), kb) {
623 + let serial = state.next_serial();
624 + let _ = kb.send_event(wl_keyboard::Event::Leave { serial, surface });
625 + }
626 + state.keyboard_focus = None;
627 + }
628 + if state.pointer_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
629 + pointer_leave(state);
630 + }
631 +}
632 +
443 633 /// Prosi klienta o zamknięcie okna (`xdg_toplevel.close`).
444 634 fn send_close(state: &mut State, slot: usize) {
445 635 let Some(surface) = state.slots.get(slot).cloned() else {
@@ -496,15 +686,19 @@ fn set_keyboard_focus(state: &mut State, slot: usize) {
496 686 }
497 687
498 688 fn keyboard_key(state: &mut State, keysym: u32, st: ButtonState) {
499 - if state.keyboard_focus.is_none() {
500 - return;
501 - }
502 689 let kb = state.keyboard.clone();
503 690 let Some(kb) = kb else { return };
504 691 sync_modifiers(state, &kb, keysym, st);
505 692 if is_modifier_keysym(keysym) {
506 693 return;
507 694 }
695 + // Skróty menedżera okien (Super + …) przechwytujemy przed klientem.
696 + if state.sent_mods.logo && window_shortcut(state, keysym, st) {
697 + return;
698 + }
699 + if state.keyboard_focus.is_none() {
700 + return;
701 + }
508 702 if let Some(key) = keysym_to_keycode(keysym) {
509 703 let serial = state.next_serial();
510 704 let time = state.time_ms();
@@ -521,6 +715,94 @@ fn keyboard_key(state: &mut State, keysym: u32, st: ButtonState) {
521 715 }
522 716 }
523 717
718 +/// Skróty menedżera okien z modyfikatorem Super. Zwraca `true`, gdy obsłużono.
719 +fn window_shortcut(state: &mut State, keysym: u32, st: ButtonState) -> bool {
720 + if st != ButtonState::Pressed {
721 + // Zjedz także zwolnienia naszych klawiszy, by nie trafiły do klienta.
722 + return matches!(keysym, 0x006d | 0x0066 | 0xff52 | 0xff54 | 0xff09);
723 + }
724 + let focused = state.keyboard_focus.as_ref().map(|(s, _)| *s);
725 + match keysym {
726 + 0x006d => {
727 + // Super+M — minimalizuj/przywróć.
728 + if let Some(s) = focused {
729 + toggle_minimize(state, s);
730 + }
731 + true
732 + }
733 + 0x0066 => {
734 + // Super+F — pełny ekran.
735 + if let Some(s) = focused {
736 + toggle_fullscreen(state, s);
737 + }
738 + true
739 + }
740 + 0xff52 => {
741 + // Super+Up — maksymalizuj/przywróć.
742 + if let Some(s) = focused {
743 + toggle_maximize(state, s);
744 + }
745 + true
746 + }
747 + 0xff54 => {
748 + // Super+Down — przywróć z maksymalizacji/fullscreenu.
749 + if let Some(s) = focused {
750 + restore_window(state, s);
751 + }
752 + true
753 + }
754 + 0xff09 => {
755 + // Super+Tab — przełącz okna (przywraca zminimalizowane).
756 + cycle_windows(state);
757 + true
758 + }
759 + _ => false,
760 + }
761 +}
762 +
763 +/// Przywraca okno z minimalizacji/maksymalizacji/fullscreenu.
764 +fn restore_window(state: &mut State, slot: usize) {
765 + let (min, max, full) = {
766 + let windows = state.windows.lock().unwrap();
767 + match windows.get(slot) {
768 + Some(w) => (w.minimized, w.maximized, w.fullscreen),
769 + None => return,
770 + }
771 + };
772 + if min {
773 + toggle_minimize(state, slot);
774 + } else if full {
775 + toggle_fullscreen(state, slot);
776 + } else if max {
777 + toggle_maximize(state, slot);
778 + }
779 +}
780 +
781 +/// Przełącza fokus na kolejne okno (przywracając je, jeśli było zminimalizowane).
782 +fn cycle_windows(state: &mut State) {
783 + let slots: Vec<usize> = {
784 + let windows = state.windows.lock().unwrap();
785 + windows
786 + .iter()
787 + .enumerate()
788 + .filter(|(_, w)| w.mapped && !w.popup && !w.layer)
789 + .map(|(i, _)| i)
790 + .collect()
791 + };
792 + if slots.is_empty() {
793 + return;
794 + }
795 + let cur = state.keyboard_focus.as_ref().map(|(s, _)| *s);
796 + let next = match cur.and_then(|c| slots.iter().position(|&s| s == c)) {
797 + Some(p) => slots[(p + 1) % slots.len()],
798 + None => slots[0],
799 + };
800 + if let Some(w) = state.windows.lock().unwrap().get_mut(next) {
801 + w.minimized = false;
802 + }
803 + raise_and_focus(state, next);
804 +}
805 +
524 806 /// Wysyła wciśnięcia/zwolnienia klawiszy modyfikujących wynikające z bieżącego stanu.
525 807 fn sync_modifiers(state: &mut State, kb: &wl_keyboard::WlKeyboard, keysym: u32, st: ButtonState) {
526 808 let mut mods = state.sent_mods;
@@ -707,23 +989,53 @@ mod tests {
707 989 popup: false,
708 990 layer: false,
709 991 mapped: true,
992 + minimized: false,
993 + maximized: false,
994 + fullscreen: false,
995 + saved: None,
710 996 data: Arc::new(ImageData::empty(200, 120)),
711 997 }
712 998 }
713 999
714 1000 #[test]
715 - fn decoration_parts_are_laid_out() {
1001 + fn window_controls_are_laid_out() {
716 1002 let w = sample_window();
717 1003 // Pasek tytułu na górze, treść pod nim.
718 1004 assert_eq!(w.titlebar().bottom(), w.y + TITLEBAR);
719 1005 assert_eq!(w.content().y(), w.y + TITLEBAR);
720 - // Przycisk zamknięcia w prawym górnym narożniku.
1006 + // Zamknij w prawym górnym narożniku.
721 1007 let c = w.close_rect();
722 1008 assert!(c.contains(Point::new(w.x + 200.0 - 1.0, w.y + 1.0)));
723 - assert!(!c.contains(Point::new(w.x + 10.0, w.y + 5.0)));
724 - // Uchwyt przeskalowania w prawym-dolnym narożniku treści.
725 - let g = w.grip_rect();
726 - let c = w.content();
727 - assert!(g.contains(Point::new(c.right() - 1.0, c.bottom() - 1.0)));
1009 + // Maksymalizuj po lewej od zamknij, minimalizuj po lewej od maksymalizuj.
1010 + let m = w.maximize_rect();
1011 + let n = w.minimize_rect();
1012 + assert!(m.right() <= c.x() + 0.01);
1013 + assert!(n.right() <= m.x() + 0.01);
1014 + }
1015 +
1016 + #[test]
1017 + fn resize_edges_detect_corners() {
1018 + let w = sample_window();
1019 + let o = w.outer();
1020 + // Lewy-górny narożnik → góra + lewo.
1021 + assert_eq!(
1022 + w.resize_edges(Point::new(o.x() + 1.0, o.y() + 1.0)),
1023 + EDGE_TOP | EDGE_LEFT
1024 + );
1025 + // Prawy-dolny narożnik → dół + prawo.
1026 + assert_eq!(
1027 + w.resize_edges(Point::new(o.right() - 1.0, o.bottom() - 1.0)),
1028 + EDGE_BOTTOM | EDGE_RIGHT
1029 + );
1030 + // Środek → brak krawędzi.
1031 + assert_eq!(w.resize_edges(o.center()), 0);
1032 + }
1033 +
1034 + #[test]
1035 + fn fullscreen_hides_titlebar() {
1036 + let mut w = sample_window();
1037 + w.fullscreen = true;
1038 + assert_eq!(w.titlebar().bottom(), w.y);
1039 + assert_eq!(w.content().y(), w.y);
728 1040 }
729 1041 }
@@ -71,6 +71,8 @@ pub struct XdgToplevelData {
71 71 pub app_id: Mutex<String>,
72 72 /// Powiązana powierzchnia (do aktualizacji modelu okien).
73 73 pub surface: WlSurface,
74 + /// Powiązany obiekt `zxdg_toplevel_decoration_v1` (jeśli klient go utworzył).
75 + pub decoration: Mutex<Option<crate::decoration::ToplevelDecoration>>,
74 76 }
75 77
76 78 impl XdgToplevelData {
@@ -79,6 +81,7 @@ impl XdgToplevelData {
79 81 title: Mutex::new(String::new()),
80 82 app_id: Mutex::new(String::new()),
81 83 surface,
84 + decoration: Mutex::new(None),
82 85 }
83 86 }
84 87 }
@@ -3,7 +3,7 @@
3 3 use std::sync::{Arc, Mutex};
4 4 use std::time::Instant;
5 5
6 -use libluna_core::{Modifiers, Rect};
6 +use libluna_core::{Modifiers, Point, 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;
@@ -13,8 +13,19 @@ use crate::monitors::{OutputInfo, Outputs};
13 13
14 14 /// Wysokość paska tytułu dekoracji (SSD), w pikselach logicznych.
15 15 pub const TITLEBAR: f64 = 30.0;
16 -/// Bok uchwytu przeskalowania w prawym-dolnym narożniku.
17 -pub const GRIP: f64 = 16.0;
16 +/// Szerokość przycisku sterowania oknem (minimalizuj/maksymalizuj/zamknij).
17 +pub const BUTTON_W: f64 = 30.0;
18 +/// Grubość krawędzi wyłapywanej do zmiany rozmiaru.
19 +pub const BORDER: f64 = 6.0;
20 +
21 +/// Zapamiętana geometria przed maksymalizacją/fullscreenem (do przywrócenia).
22 +#[derive(Debug, Clone, Copy)]
23 +pub struct SavedGeom {
24 + pub x: f64,
25 + pub y: f64,
26 + pub width: i32,
27 + pub height: i32,
28 +}
18 29
19 30 /// Okno (toplevel/popup) w modelu kompozycji: pozycja, rozmiar, kolejność i piksele.
20 31 pub struct Window {
@@ -33,14 +44,22 @@ pub struct Window {
33 44 pub layer: bool,
34 45 /// Czy powierzchnia jest widoczna (popup znika po `popup_done`).
35 46 pub mapped: bool,
47 + /// Zminimalizowane (ukryte przez kompozytor; przywracane skrótem).
48 + pub minimized: bool,
49 + /// Zmaksymalizowane do obszaru monitora.
50 + pub maximized: bool,
51 + /// Pełny ekran.
52 + pub fullscreen: bool,
53 + /// Geometria zapamiętana przed maksymalizacją/fullscreenem.
54 + pub saved: Option<SavedGeom>,
36 55 /// Piksele (premultiplied RGBA8) ostatnio zacommitowanego bufora.
37 56 pub data: Arc<ImageData>,
38 57 }
39 58
40 59 impl Window {
41 - /// Wysokość dekoracji (zero dla popupów i powierzchni warstwowych).
60 + /// Wysokość dekoracji (zero dla popupów, paneli i pełnego ekranu).
42 61 fn bar(&self) -> f64 {
43 - if self.popup || self.layer {
62 + if self.popup || self.layer || self.fullscreen {
44 63 0.0
45 64 } else {
46 65 TITLEBAR
@@ -72,26 +91,58 @@ impl Window {
72 91 )
73 92 }
74 93
75 - /// Kwadratowy przycisk zamknięcia (tylko dla topleveli).
76 - pub fn close_rect(&self) -> Rect {
94 + /// Przycisk sterowania oknem licząc od prawej (0 = zamknij, 1 = maksymalizuj, 2 = minimalizuj).
95 + fn control_rect(&self, from_right: usize) -> Rect {
77 96 if self.popup || self.layer {
78 97 return Rect::ZERO;
79 98 }
80 99 Rect::from_xywh(
81 - self.x + self.width as f64 - TITLEBAR,
100 + self.x + self.width as f64 - BUTTON_W * (from_right as f64 + 1.0),
82 101 self.y,
83 - TITLEBAR,
102 + BUTTON_W,
84 103 TITLEBAR,
85 104 )
86 105 }
87 106
88 - /// Uchwyt przeskalowania (prawy-dolny narożnik treści; tylko dla topleveli).
89 - pub fn grip_rect(&self) -> Rect {
90 - if self.popup || self.layer {
91 - return Rect::ZERO;
107 + /// Kwadratowy przycisk zamknięcia (tylko dla topleveli).
108 + pub fn close_rect(&self) -> Rect {
109 + self.control_rect(0)
110 + }
111 +
112 + /// Przycisk maksymalizacji.
113 + pub fn maximize_rect(&self) -> Rect {
114 + self.control_rect(1)
115 + }
116 +
117 + /// Przycisk minimalizacji.
118 + pub fn minimize_rect(&self) -> Rect {
119 + self.control_rect(2)
120 + }
121 +
122 + /// Bitmaska krawędzi wyłapywanej do zmiany rozmiaru dla punktu (0 gdy brak).
123 + /// 1 = góra, 2 = dół, 4 = lewo, 8 = prawo.
124 + pub fn resize_edges(&self, p: Point) -> u8 {
125 + if self.popup || self.layer || self.fullscreen || self.maximized {
126 + return 0;
127 + }
128 + let o = self.outer();
129 + if !o.contains(p) {
130 + return 0;
131 + }
132 + let mut e = 0u8;
133 + if p.x <= o.x() + BORDER {
134 + e |= 4;
135 + }
136 + if p.x >= o.right() - BORDER {
137 + e |= 8;
138 + }
139 + if p.y <= o.y() + BORDER {
140 + e |= 1;
141 + }
142 + if p.y >= o.bottom() - BORDER {
143 + e |= 2;
92 144 }
93 - let c = self.content();
94 - Rect::from_xywh(c.right() - GRIP, c.bottom() - GRIP, GRIP, GRIP)
145 + e
95 146 }
96 147 }
97 148
@@ -107,10 +158,14 @@ pub struct DragState {
107 158 pub off_y: f64,
108 159 }
109 160
110 -/// Trwające przeskalowanie okna (uchwyt w prawym-dolnym narożniku).
161 +/// Trwające przeskalowanie okna (za dowolną krawędź/narożnik).
111 162 #[derive(Debug, Clone, Copy)]
112 163 pub struct ResizeState {
113 164 pub slot: usize,
165 + /// Bitmaska krawędzi (1=góra, 2=dół, 4=lewo, 8=prawo).
166 + pub edges: u8,
167 + pub start_x: f64,
168 + pub start_y: f64,
114 169 pub start_w: f64,
115 170 pub start_h: f64,
116 171 pub start_px: f64,