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,