← luna

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 }