Commit 3a8837e
4
files
+308
added
-8
deleted
@@ -77,6 +77,7 @@ fn main() -> Result<(), Box<dyn Error>> {
77
77
}
78
78
}
79
79
let output_count = state.outputs.lock_or_recover().len();
80
+ let ws_count = state.ws_count;
80
81
81
82
// Globalne interfejsy protokołu.
82
83
handle.create_global::<State, wl_compositor::WlCompositor, ()>(4, ());
@@ -115,6 +116,8 @@ fn main() -> Result<(), Box<dyn Error>> {
115
116
Arc::clone(&state.windows),
116
117
Arc::clone(&state.cursor),
117
118
Arc::clone(&state.ws_current),
119
+ Arc::clone(&state.overview),
120
+ ws_count,
118
121
);
119
122
} else {
120
123
output::spawn(
@@ -123,6 +126,8 @@ fn main() -> Result<(), Box<dyn Error>> {
123
126
Arc::clone(&events),
124
127
wake_write,
125
128
Arc::clone(&state.ws_current),
129
+ Arc::clone(&state.overview),
130
+ ws_count,
126
131
1280,
127
132
800,
128
133
);
@@ -15,7 +15,7 @@ use libluna_render::{Scene, SoftwareRenderer, TextRun};
15
15
use crate::drm::{DrmBackend, DrmError};
16
16
use crate::input::{self, Events};
17
17
use crate::monitors::Outputs;
18
-use crate::state::{CursorModel, Window, WindowModel, WorkspaceModel};
18
+use crate::state::{CursorModel, OverviewModel, Window, WindowModel, WorkspaceModel};
19
19
use crate::sync::LockExt;
20
20
21
21
/// Uruchamia wątek wyjścia (nested Wayland) o zadanym rozmiarze.
@@ -25,6 +25,8 @@ pub fn spawn(
25
25
events: Events,
26
26
wake: UnixStream,
27
27
ws: WorkspaceModel,
28
+ overview: OverviewModel,
29
+ ws_count: usize,
28
30
width: u32,
29
31
height: u32,
30
32
) {
@@ -48,7 +50,19 @@ pub fn spawn(
48
50
// Narysuj scenę.
49
51
let size = frame.size;
50
52
let cur_ws = *ws.lock_or_recover();
51
- build_scene(frame.scene, size, 0.0, 0.0, &model, cur_ws, None);
53
+ let ov = *overview.lock_or_recover();
54
+ build_scene(
55
+ frame.scene,
56
+ size,
57
+ 0.0,
58
+ 0.0,
59
+ &model,
60
+ cur_ws,
61
+ ws_count,
62
+ ov,
63
+ true,
64
+ None,
65
+ );
52
66
// Utrzymuj klatki — model okien zmienia się poza tą pętlą.
53
67
frame.request_frame();
54
68
});
@@ -91,7 +105,13 @@ fn open_drm_waiting() -> Result<DrmBackend, DrmError> {
91
105
///
92
106
/// Wymaga bycia DRM masterem; jeśli inną sesję trzyma master, kończy bez efektu
93
107
/// (wywołujący powinien wtedy użyć wyjścia nested).
94
-pub fn spawn_drm(model: WindowModel, cursor: CursorModel, ws: WorkspaceModel) {
108
+pub fn spawn_drm(
109
+ model: WindowModel,
110
+ cursor: CursorModel,
111
+ ws: WorkspaceModel,
112
+ overview: OverviewModel,
113
+ ws_count: usize,
114
+) {
95
115
std::thread::spawn(move || {
96
116
let mut backend = match open_drm_waiting() {
97
117
Ok(backend) => backend,
@@ -171,10 +191,11 @@ pub fn spawn_drm(model: WindowModel, cursor: CursorModel, ws: WorkspaceModel) {
171
191
loop {
172
192
let cur = *cursor.lock_or_recover();
173
193
let cur_ws = *ws.lock_or_recover();
194
+ let ov = *overview.lock_or_recover();
174
195
175
196
// Sygnatura sceny nie zależy od kursora, gdy rysuje go sprzęt — dzięki temu
176
197
// samo przesunięcie myszy nie powoduje przemalowania sceny.
177
- let sig = scene_signature(&model, cur_ws, if hw_cursor { None } else { cur });
198
+ let sig = scene_signature(&model, cur_ws, ov, if hw_cursor { None } else { cur });
178
199
let scene_changed = last_sig != Some(sig);
179
200
let cur_changed = last_cur != cur;
180
201
if !scene_changed && !cur_changed {
@@ -225,6 +246,9 @@ pub fn spawn_drm(model: WindowModel, cursor: CursorModel, ws: WorkspaceModel) {
225
246
oy,
226
247
&model,
227
248
cur_ws,
249
+ ws_count,
250
+ ov,
251
+ i == 0,
228
252
if hw_cursor { None } else { cur },
229
253
);
230
254
renderers[i].draw(&scene, 1.0, bg);
@@ -282,14 +306,20 @@ fn cursor_image() -> (Vec<u8>, u32, u32) {
282
306
(renderer.data().to_vec(), w, h)
283
307
}
284
308
285
-/// Sygnatura sceny (okna + kursor + przestrzeń) — do wykrywania, że nic się nie zmieniło.
286
-fn scene_signature(model: &WindowModel, current_ws: usize, cursor: Option<Point>) -> u64 {
309
+/// Sygnatura sceny (okna + kursor + przestrzeń + podgląd) — do wykrywania zmian.
310
+fn scene_signature(
311
+ model: &WindowModel,
312
+ current_ws: usize,
313
+ overview: bool,
314
+ cursor: Option<Point>,
315
+) -> u64 {
287
316
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
288
317
let mut mix = |v: u64| {
289
318
h ^= v;
290
319
h = h.wrapping_mul(0x0000_0100_0000_01b3);
291
320
};
292
321
mix(current_ws as u64);
322
+ mix(u64::from(overview));
293
323
{
294
324
let windows = model.lock_or_recover();
295
325
mix(windows.len() as u64);
@@ -311,6 +341,7 @@ fn scene_signature(model: &WindowModel, current_ws: usize, cursor: Option<Point>
311
341
}
312
342
313
343
/// Buduje scenę okien w rozmiarze wyjścia, przesuniętą o `(ox, oy)` (origin monitora).
344
+#[allow(clippy::too_many_arguments)]
314
345
fn build_scene(
315
346
scene: &mut Scene,
316
347
size: Size,
@@ -318,6 +349,9 @@ fn build_scene(
318
349
oy: f64,
319
350
model: &WindowModel,
320
351
current_ws: usize,
352
+ ws_count: usize,
353
+ overview: bool,
354
+ is_primary: bool,
321
355
cursor: Option<Point>,
322
356
) {
323
357
let sh = |r: Rect| r.offset(-ox, -oy);
@@ -399,6 +433,11 @@ fn build_scene(
399
433
.with_color(Color::from_rgba8(0xdd, 0xdd, 0xdd, 0xff)),
400
434
);
401
435
436
+ // Podgląd okien i przestrzeni (Super+Tab) — rysowany na głównym monitorze.
437
+ if overview && is_primary {
438
+ draw_overview(scene, size, model, current_ws, ws_count);
439
+ }
440
+
402
441
// Kursor rysuje ten monitor, na którym leży (tryb DRM — nikt go za nas nie narysuje).
403
442
if let Some(c) = cursor {
404
443
let (cx, cy) = (c.x - ox, c.y - oy);
@@ -408,6 +447,66 @@ fn build_scene(
408
447
}
409
448
}
410
449
450
+/// Rysuje podgląd okien i przestrzeni — kafelki z podglądem treści + pasek przestrzeni.
451
+fn draw_overview(
452
+ scene: &mut Scene,
453
+ size: Size,
454
+ model: &WindowModel,
455
+ current_ws: usize,
456
+ ws_count: usize,
457
+) {
458
+ // Przygaszenie tła.
459
+ scene.fill_rect(
460
+ Rect::from_size(size),
461
+ Color::from_rgba8(0x0b, 0x0d, 0x13, 0xd6),
462
+ );
463
+ let slots = {
464
+ let w = model.lock_or_recover();
465
+ crate::state::overview_slots(&w, current_ws)
466
+ };
467
+ let tiles = crate::state::overview_tiles(slots.len(), (size.width, size.height));
468
+ let windows = model.lock_or_recover();
469
+ for (k, &slot) in slots.iter().enumerate() {
470
+ let (Some(r), Some(w)) = (tiles.get(k), windows.get(slot)) else {
471
+ continue;
472
+ };
473
+ let title_h = crate::state::OVERVIEW_TITLE;
474
+ scene.fill_rounded_rect(*r, 8.0, Color::from_rgba8(0x1a, 0x1d, 0x25, 0xff));
475
+ let bar = Rect::from_xywh(r.x(), r.y(), r.width(), title_h);
476
+ scene.fill_rounded_rect(bar, 8.0, Color::from_rgba8(0x2c, 0x2f, 0x38, 0xff));
477
+ let img = Rect::from_xywh(
478
+ r.x(),
479
+ r.y() + title_h,
480
+ r.width(),
481
+ (r.height() - title_h).max(1.0),
482
+ );
483
+ scene.image(img, w.data.clone());
484
+ scene.stroke_rounded_rect(*r, 8.0, 1.5, Color::from_rgba8(0x6c, 0x74, 0x86, 0xff));
485
+ scene.text(
486
+ TextRun::new(Point::new(r.x() + 10.0, r.y() + 18.0), &w.title, 13.0)
487
+ .with_color(Color::from_rgba8(0xee, 0xee, 0xee, 0xff)),
488
+ );
489
+ }
490
+ // Pasek przestrzeni roboczych.
491
+ let chips = crate::state::overview_ws_chips(ws_count, (size.width, size.height));
492
+ for (i, c) in chips.iter().enumerate() {
493
+ let bg = if i == current_ws {
494
+ Color::from_rgba8(0x5b, 0x8d, 0xef, 0xff)
495
+ } else {
496
+ Color::from_rgba8(0x2c, 0x2f, 0x38, 0xff)
497
+ };
498
+ scene.fill_rounded_rect(*c, c.height() / 2.0, bg);
499
+ scene.text(
500
+ TextRun::new(
501
+ Point::new(c.center().x - 6.0, c.center().y + 6.0),
502
+ &format!("{}", i + 1),
503
+ 16.0,
504
+ )
505
+ .with_color(Color::from_rgba8(0xf5, 0xf5, 0xf5, 0xff)),
506
+ );
507
+ }
508
+}
509
+
411
510
/// Klasyczna strzałka kursora (biała z ciemnym obrysem).
412
511
fn draw_cursor(scene: &mut Scene, p: Point) {
413
512
let arrow = [
@@ -464,6 +563,9 @@ mod tests {
464
563
0.0,
465
564
&model,
466
565
0,
566
+ 1,
567
+ false,
568
+ true,
467
569
Some(Point::new(100.0, 100.0)),
468
570
);
469
571
renderer.draw(&scene, 1.0, bg);
@@ -516,6 +618,9 @@ mod tests {
516
618
0.0,
517
619
&model,
518
620
0,
621
+ 1,
622
+ false,
623
+ true,
519
624
Some(Point::new(100.0, 100.0)),
520
625
);
521
626
renderer.draw(&scene, 1.0, bg);
@@ -329,6 +329,13 @@ fn pointer_button(
329
329
button: PointerButton,
330
330
st: ButtonState,
331
331
) {
332
+ // W trybie podglądu klik wybiera kafelek/chip lub zamyka podgląd.
333
+ if state.overview_on() {
334
+ if st == ButtonState::Pressed && button == PointerButton::Left {
335
+ overview_click(state, pos);
336
+ }
337
+ return;
338
+ }
332
339
if st == ButtonState::Released {
333
340
state.drag = None;
334
341
state.resize = None;
@@ -772,6 +779,11 @@ fn keyboard_key(state: &mut State, keysym: u32, st: ButtonState) {
772
779
if is_modifier_keysym(keysym) {
773
780
return;
774
781
}
782
+ // Escape zamyka podgląd (Super+Tab) i nie trafia do klienta.
783
+ if keysym == 0xff1b && state.overview_on() {
784
+ state.set_overview(false);
785
+ return;
786
+ }
775
787
// Skróty menedżera okien (Super + …) przechwytujemy przed klientem.
776
788
if state.sent_mods.logo && window_shortcut(state, keysym, st) {
777
789
return;
@@ -801,7 +813,7 @@ fn window_shortcut(state: &mut State, keysym: u32, st: ButtonState) -> bool {
801
813
// Zjedz także zwolnienia naszych klawiszy, by nie trafiły do klienta.
802
814
return matches!(
803
815
keysym,
804
- 0x006d | 0x0066 | 0x006c | 0x004c | 0xff52 | 0xff54 | 0xff09 | 0x0031..=0x0038
816
+ 0x006d | 0x0066 | 0x006c | 0x004c | 0xff52 | 0xff54 | 0xff09 | 0x0060 | 0x0031..=0x0038
805
817
);
806
818
}
807
819
let focused = state.keyboard_focus.as_ref().map(|(s, _)| *s);
@@ -841,7 +853,13 @@ fn window_shortcut(state: &mut State, keysym: u32, st: ButtonState) -> bool {
841
853
true
842
854
}
843
855
0xff09 => {
844
- // Super+Tab — przełącz okna (przywraca zminimalizowane).
856
+ // Super+Tab — podgląd okien i przestrzeni.
857
+ let on = state.overview_on();
858
+ state.set_overview(!on);
859
+ true
860
+ }
861
+ 0x0060 => {
862
+ // Super+` — przełącz okna (przywraca zminimalizowane).
845
863
cycle_windows(state);
846
864
true
847
865
}
@@ -965,6 +983,41 @@ fn move_window_to_workspace(state: &mut State, slot: usize, ws: usize) {
965
983
crate::workspace::broadcast_workspace_states(state);
966
984
}
967
985
986
+/// Rozmiar głównego monitora — obszar układu podglądu.
987
+fn primary_size(state: &State) -> (f64, f64) {
988
+ state
989
+ .outputs
990
+ .lock_or_recover()
991
+ .first()
992
+ .map(|o| (o.width as f64, o.height as f64))
993
+ .unwrap_or((1280.0, 800.0))
994
+}
995
+
996
+/// Klik w trybie podglądu: kafelek → fokus okna, chip → przestrzeń, poza → zamknij.
997
+fn overview_click(state: &mut State, pos: Point) {
998
+ let size = primary_size(state);
999
+ let slots = {
1000
+ let w = state.windows.lock_or_recover();
1001
+ crate::state::overview_slots(&w, *state.ws_current.lock_or_recover())
1002
+ };
1003
+ let tiles = crate::state::overview_tiles(slots.len(), size);
1004
+ if let Some(k) = tiles.iter().position(|r| r.contains(pos)) {
1005
+ if let Some(&slot) = slots.get(k) {
1006
+ state.set_overview(false);
1007
+ raise_and_focus(state, slot);
1008
+ return;
1009
+ }
1010
+ }
1011
+ let chips = crate::state::overview_ws_chips(state.ws_count, size);
1012
+ if let Some(i) = chips.iter().position(|r| r.contains(pos)) {
1013
+ state.set_overview(false);
1014
+ switch_workspace(state, i);
1015
+ return;
1016
+ }
1017
+ // Klik poza kafelkami — zamknij podgląd.
1018
+ state.set_overview(false);
1019
+}
1020
+
968
1021
/// Wysyła wciśnięcia/zwolnienia klawiszy modyfikujących wynikające z bieżącego stanu.
969
1022
fn sync_modifiers(state: &mut State, kb: &wl_keyboard::WlKeyboard, keysym: u32, st: ButtonState) {
970
1023
let mut mods = state.sent_mods;
@@ -18,6 +18,7 @@ use wayland_server::protocol::wl_surface::WlSurface;
18
18
19
19
use crate::data::DndState;
20
20
use crate::monitors::{OutputInfo, Outputs};
21
+use crate::sync::LockExt;
21
22
22
23
/// Wysokość paska tytułu dekoracji (SSD), w pikselach logicznych.
23
24
pub const TITLEBAR: f64 = 30.0;
@@ -173,6 +174,76 @@ pub type CursorModel = Arc<Mutex<Option<Point>>>;
173
174
/// Współdzielony numer bieżącej przestrzeni roboczej (kompozytor ↔ wątek wyjścia).
174
175
pub type WorkspaceModel = Arc<Mutex<usize>>;
175
176
177
+/// Współdzielony tryb podglądu okien/przestrzeni (kompozytor ↔ wątek wyjścia).
178
+pub type OverviewModel = Arc<Mutex<bool>>;
179
+
180
+/// Margines podglądu (od krawędzi ekranu).
181
+pub const OVERVIEW_PAD: f64 = 56.0;
182
+/// Odstęp między kafelkami podglądu.
183
+pub const OVERVIEW_GAP: f64 = 16.0;
184
+/// Wysokość pasa „chipsów” przestrzeni u dołu podglądu.
185
+pub const OVERVIEW_STRIP_H: f64 = 56.0;
186
+/// Wysokość paska tytułu kafelka.
187
+pub const OVERVIEW_TITLE: f64 = 26.0;
188
+
189
+/// Sloty okien bieżącej przestrzeni (bez paneli/popupów), w kolejności z-order.
190
+pub fn overview_slots(windows: &[Window], current_ws: usize) -> Vec<usize> {
191
+ let mut v: Vec<(usize, i32)> = windows
192
+ .iter()
193
+ .enumerate()
194
+ .filter(|(_, w)| {
195
+ w.mapped && !w.minimized && !w.popup && !w.layer && w.workspace == current_ws
196
+ })
197
+ .map(|(i, w)| (i, w.z))
198
+ .collect();
199
+ v.sort_by_key(|&(_, z)| z);
200
+ v.into_iter().map(|(i, _)| i).collect()
201
+}
202
+
203
+/// Prostokąty kafelków w siatce na obszarze `size` (zostawiając pas u dołu).
204
+pub fn overview_tiles(count: usize, size: (f64, f64)) -> Vec<Rect> {
205
+ if count == 0 {
206
+ return Vec::new();
207
+ }
208
+ let cols = (count as f64).sqrt().ceil() as usize;
209
+ let cols = cols.max(1);
210
+ let rows = count.div_ceil(cols);
211
+ let avail_w = (size.0 - 2.0 * OVERVIEW_PAD).max(1.0);
212
+ let avail_h = (size.1 - 2.0 * OVERVIEW_PAD - OVERVIEW_STRIP_H).max(1.0);
213
+ let tw = ((avail_w - (cols - 1) as f64 * OVERVIEW_GAP) / cols as f64).max(1.0);
214
+ let th = ((avail_h - (rows - 1) as f64 * OVERVIEW_GAP) / rows as f64).max(1.0);
215
+ let grid_w = cols as f64 * tw + (cols - 1) as f64 * OVERVIEW_GAP;
216
+ let grid_h = rows as f64 * th + (rows - 1) as f64 * OVERVIEW_GAP;
217
+ let x0 = (size.0 - grid_w) / 2.0;
218
+ let y0 = OVERVIEW_PAD + ((avail_h - grid_h) / 2.0).max(0.0);
219
+ (0..count)
220
+ .map(|k| {
221
+ let c = (k % cols) as f64;
222
+ let r = (k / cols) as f64;
223
+ Rect::from_xywh(
224
+ x0 + c * (tw + OVERVIEW_GAP),
225
+ y0 + r * (th + OVERVIEW_GAP),
226
+ tw,
227
+ th,
228
+ )
229
+ })
230
+ .collect()
231
+}
232
+
233
+/// Prostokąty „chipsów” przestrzeni na dole podglądu.
234
+pub fn overview_ws_chips(count: usize, size: (f64, f64)) -> Vec<Rect> {
235
+ if count == 0 {
236
+ return Vec::new();
237
+ }
238
+ let (w, h, gap) = (128.0, 36.0, 12.0);
239
+ let total = count as f64 * w + (count - 1) as f64 * gap;
240
+ let x0 = (size.0 - total) / 2.0;
241
+ let y = size.1 - OVERVIEW_PAD - h;
242
+ (0..count)
243
+ .map(|i| Rect::from_xywh(x0 + i as f64 * (w + gap), y, w, h))
244
+ .collect()
245
+}
246
+
176
247
/// Trwające przeciąganie okna.
177
248
#[derive(Debug, Clone, Copy)]
178
249
pub struct DragState {
@@ -224,6 +295,8 @@ pub struct State {
224
295
pub ws_count: usize,
225
296
/// Bieżąca przestrzeń robocza (współdzielona z wątkiem wyjścia).
226
297
pub ws_current: WorkspaceModel,
298
+ /// Tryb podglądu okien/przestrzeni (`Super+Tab`), współdzielony z wątkiem wyjścia.
299
+ pub overview: OverviewModel,
227
300
/// Podłączeni menedżerowie `ext_workspace` oraz ich grupy i uchwyty przestrzeni.
228
301
pub ws_managers: Vec<ExtWorkspaceManagerV1>,
229
302
pub ws_groups: Vec<ExtWorkspaceGroupHandleV1>,
@@ -266,6 +339,7 @@ impl Default for State {
266
339
toplevel_managers: Vec::new(),
267
340
ws_count: 4,
268
341
ws_current: Arc::new(Mutex::new(0)),
342
+ overview: Arc::new(Mutex::new(false)),
269
343
ws_managers: Vec::new(),
270
344
ws_groups: Vec::new(),
271
345
ws_handles: Vec::new(),
@@ -301,6 +375,16 @@ impl State {
301
375
self.z_counter += 1;
302
376
self.z_counter
303
377
}
378
+
379
+ /// Czy trwa podgląd okien/przestrzeni.
380
+ pub fn overview_on(&self) -> bool {
381
+ *self.overview.lock_or_recover()
382
+ }
383
+
384
+ /// Włącza/wyłącza podgląd okien/przestrzeni.
385
+ pub fn set_overview(&self, on: bool) {
386
+ *self.overview.lock_or_recover() = on;
387
+ }
304
388
}
305
389
306
390
#[cfg(test)]
@@ -323,4 +407,57 @@ mod tests {
323
407
let b = s.next_z();
324
408
assert!(b > a);
325
409
}
410
+
411
+ fn window_at_ws(ws: usize, z: i32) -> Window {
412
+ Window {
413
+ x: 0.0,
414
+ y: 0.0,
415
+ width: 10,
416
+ height: 10,
417
+ z,
418
+ title: String::new(),
419
+ popup: false,
420
+ layer: false,
421
+ mapped: true,
422
+ workspace: ws,
423
+ minimized: false,
424
+ maximized: false,
425
+ fullscreen: false,
426
+ saved: None,
427
+ data: std::sync::Arc::new(libluna_render::ImageData::empty(1, 1)),
428
+ }
429
+ }
430
+
431
+ #[test]
432
+ fn overview_slots_filter_and_order() {
433
+ let mut panel = window_at_ws(0, 100);
434
+ panel.layer = true;
435
+ let ws = vec![
436
+ window_at_ws(0, 5),
437
+ window_at_ws(1, 9),
438
+ window_at_ws(0, 1),
439
+ panel,
440
+ ];
441
+ // Tylko okna bieżącej przestrzeni, bez panelu, w kolejności z.
442
+ assert_eq!(overview_slots(&ws, 0), vec![2, 0]);
443
+ assert_eq!(overview_slots(&ws, 1), vec![1]);
444
+ }
445
+
446
+ #[test]
447
+ fn overview_tiles_and_chips_layout() {
448
+ let size = (1920.0, 1080.0);
449
+ assert!(overview_tiles(0, size).is_empty());
450
+ let four = overview_tiles(4, size);
451
+ assert_eq!(four.len(), 4);
452
+ // Drugi kafelek na prawo od pierwszego i bez nachodzenia.
453
+ assert!(four[1].x() > four[0].x());
454
+ assert!(four[0].right() <= four[1].x());
455
+ for r in &four {
456
+ assert!(r.x() >= 0.0 && r.right() <= size.0 + 0.5);
457
+ assert!(r.bottom() <= size.1 + 0.5);
458
+ }
459
+ let chips = overview_ws_chips(4, size);
460
+ assert_eq!(chips.len(), 4);
461
+ assert!(chips[0].x() < chips[3].x());
462
+ }
326
463
}