← luna

Commit 629a644

14
files
+636
added
-23
deleted
@@ -609,6 +609,7 @@ dependencies = [
609 609 "libluna-render",
610 610 "libluna-widgets",
611 611 "wayland-client",
612 + "wayland-protocols",
612 613 "wayland-protocols-wlr",
613 614 "zbus",
614 615 ]
@@ -31,5 +31,6 @@ libluna-i18n.workspace = true
31 31 libluna-platform.workspace = true
32 32
33 33 wayland-client = "0.31"
34 +wayland-protocols = { version = "0.32", features = ["client", "staging"] }
34 35 wayland-protocols-wlr = { version = "0.3", features = ["client"] }
35 36 zbus = { version = "5.12.0", features = ["blocking-api"] }
@@ -33,8 +33,7 @@ fn main() -> Result<(), Box<dyn Error>> {
33 33 .transparent(true);
34 34
35 35 let mut state = PanelState::sample();
36 - // Selene nie ma jeszcze przestrzeni roboczych — nie pokazuj fałszywych kropek.
37 - state.workspaces = 0;
36 + // Liczba/bieżąca przestrzeń ustawiane co klatkę z kompozytora (ext-workspace).
38 37 // Prawdziwe aplikacje z plików `.desktop`; gdy brak (np. kontener) — zestaw demo.
39 38 let desktop = luna::apps::scan();
40 39 let apps = if desktop.is_empty() {
@@ -54,6 +53,8 @@ fn main() -> Result<(), Box<dyn Error>> {
54 53 let mut bus_tried = false;
55 54 // Lista okien z kompozytora (foreign-toplevel) — własne połączenie + wątek.
56 55 let toplevels = luna::toplevels::Toplevels::spawn();
56 + // Przestrzenie robocze z kompozytora (ext-workspace) — własne połączenie + wątek.
57 + let workspaces = luna::workspaces::Workspaces::spawn();
57 58 // Realny stan systemu (użytkownik, dźwięk, Wi-Fi, bateria) — odpytywany w tle.
58 59 let system = luna::system::spawn();
59 60 // Sygnatura stanu — przerysowujemy tylko, gdy coś się zmieniło.
@@ -94,6 +95,18 @@ fn main() -> Result<(), Box<dyn Error>> {
94 95 state.user = sys.user.clone();
95 96 state.toggles = build_toggles(&sys);
96 97 }
98 + // Przestrzenie robocze z kompozytora.
99 + match &workspaces {
100 + Some(ws) => {
101 + let list = ws.list();
102 + state.workspaces = list.len();
103 + state.current_ws = list.iter().position(|w| w.active).unwrap_or(0);
104 + }
105 + None => {
106 + state.workspaces = 0;
107 + state.current_ws = 0;
108 + }
109 + }
97 110
98 111 let lw = LAUNCHER_W.min((size.width - 80.0).max(320.0));
99 112 let larea = Rect::from_xywh((size.width - lw) / 2.0, THICKNESS + 24.0, lw, LAUNCHER_H);
@@ -163,6 +176,13 @@ fn main() -> Result<(), Box<dyn Error>> {
163 176 ToggleAction::Volume => luna::system::toggle_mute(),
164 177 ToggleAction::None => {}
165 178 }
179 + } else if let Some(i) =
180 + panel::workspace_at(bar, &frame.theme, text, &state, pe.position)
181 + {
182 + // Klik w kropkę przestrzeni — przełącz w kompozytorze.
183 + if let Some(ws) = &workspaces {
184 + ws.activate(i);
185 + }
166 186 }
167 187 }
168 188 Event::PointerButton(pe)
@@ -268,6 +288,9 @@ fn main() -> Result<(), Box<dyn Error>> {
268 288 th = th
269 289 .wrapping_mul(1_099_511_628_211)
270 290 .wrapping_add(state.user.len() as u64);
291 + th = th
292 + .wrapping_mul(1_099_511_628_211)
293 + .wrapping_add(state.workspaces as u64 ^ ((state.current_ws as u64) << 8));
271 294 let sig = (open, power_open, state.clock.clone(), th);
272 295 let animating = osd.visible() || notes.visible();
273 296 if !frame.events.is_empty() || animating || open || sig != last_sig {
@@ -14,3 +14,4 @@ pub mod panel;
14 14 pub mod power;
15 15 pub mod system;
16 16 pub mod toplevels;
17 +pub mod workspaces;
@@ -321,6 +321,43 @@ pub fn toggle_at(
321 321 .position(|r| r.contains(p))
322 322 }
323 323
324 +/// Prostokąty kropek przestrzeni roboczych (spójne z `paint_panel`), w kolejności `state.workspaces`.
325 +pub fn workspace_rects(
326 + bar: Rect,
327 + theme: &Theme,
328 + text: &TextEngine,
329 + state: &PanelState,
330 +) -> Vec<Rect> {
331 + let t = theme;
332 + let cy = bar.center().y;
333 + let d = 8.0;
334 + let toggles = toggle_rects(bar, t, text, state);
335 + let mut right = toggles
336 + .last()
337 + .map(|r| r.x() - t.spacing.sm)
338 + .unwrap_or(bar.right() - t.spacing.md);
339 + let mut rects = vec![Rect::ZERO; state.workspaces];
340 + for i in (0..state.workspaces).rev() {
341 + let dot = Rect::from_xywh(right - d, cy - d / 2.0, d, d);
342 + rects[i] = dot;
343 + right = dot.x() - t.spacing.sm;
344 + }
345 + rects
346 +}
347 +
348 +/// Indeks kropki przestrzeni roboczej pod punktem `p`.
349 +pub fn workspace_at(
350 + bar: Rect,
351 + theme: &Theme,
352 + text: &TextEngine,
353 + state: &PanelState,
354 + p: Point,
355 +) -> Option<usize> {
356 + workspace_rects(bar, theme, text, state)
357 + .iter()
358 + .position(|r| r.contains(p))
359 +}
360 +
324 361 /// Prostokąt rozwijanego menu zasilania (pod przyciskiem, wyrównany do prawej).
325 362 pub fn power_menu_rect(bar: Rect, theme: &Theme, items: usize) -> Rect {
326 363 let p = power_rect(bar, theme);
@@ -562,27 +599,21 @@ pub fn paint_panel(panel: &PanelState, ctx: &mut PaintCtx, rect: Rect) {
562 599 right -= uw + t.spacing.md;
563 600 ctx.scene
564 601 .fill_rect(Rect::from_xywh(right, cy - 8.0, 1.0, 16.0), t.colors.border);
565 - right -= t.spacing.md;
566 602
567 603 // Przełączniki szybkiego dostępu (pigułki).
568 604 let toggles = toggle_rects(rect, t, ctx.text, panel);
569 605 for (r, toggle) in toggles.iter().zip(panel.toggles.iter()) {
570 606 paint_toggle_pill(ctx, *r, toggle);
571 607 }
572 - if let Some(last) = toggles.last() {
573 - right = last.x() - t.spacing.sm;
574 - }
575 608
576 - for i in (0..panel.workspaces).rev() {
577 - let d = 8.0;
578 - let dot = Rect::from_xywh(right - d, cy - d / 2.0, d, d);
609 + // Kropki przestrzeni roboczych (spójne z `workspace_rects`).
610 + for (i, dot) in workspace_rects(rect, t, ctx.text, panel).iter().enumerate() {
579 611 let color = if i == panel.current_ws {
580 612 t.colors.accent
581 613 } else {
582 614 t.colors.dim_text
583 615 };
584 - ctx.scene.fill_rounded_rect(dot, d / 2.0, color);
585 - right = dot.x() - t.spacing.sm;
616 + ctx.scene.fill_rounded_rect(*dot, dot.height() / 2.0, color);
586 617 }
587 618 }
588 619
@@ -1097,6 +1128,24 @@ mod tests {
1097 1128 Rect::from_xywh(100.0, 60.0, LAUNCHER_W, LAUNCHER_H)
1098 1129 }
1099 1130
1131 + #[test]
1132 + fn workspace_dots_hit_test_matches_layout() {
1133 + let theme = Theme::dark();
1134 + let text = TextEngine::none();
1135 + let bar = Rect::from_xywh(0.0, 0.0, 1280.0, THICKNESS);
1136 + let mut st = PanelState::sample();
1137 + st.workspaces = 4;
1138 + st.current_ws = 2;
1139 + let rects = workspace_rects(bar, &theme, &text, &st);
1140 + assert_eq!(rects.len(), 4);
1141 + for (i, r) in rects.iter().enumerate() {
1142 + assert_eq!(workspace_at(bar, &theme, &text, &st, r.center()), Some(i));
1143 + }
1144 + // Kropki idą w lewo (wyższy indeks bliżej prawej) i nie zachodzą na siebie.
1145 + assert!(rects[3].x() > rects[0].x());
1146 + assert!(rects[0].right() <= rects[1].x());
1147 + }
1148 +
1100 1149 #[test]
1101 1150 fn power_menu_items_hit_test() {
1102 1151 let theme = Theme::dark();
1103 1152 new file mode 100644
@@ -0,0 +1,181 @@
1 +//! Klient `ext_workspace_v1` — lista przestrzeni roboczych dla paska.
2 +//!
3 +//! Powłoka otwiera **własne** połączenie Wayland (jak dla listy okien) i binduje
4 +//! menedżera przestrzeni. W osobnym wątku śledzi uchwyty (id, nazwa, stan `active`),
5 +//! a UI czyta gotową listę przez [`Workspaces::list`] i przełącza przez
6 +//! [`Workspaces::activate`] — dzięki temu kropki w pasku odzwierciedlają realny stan
7 +//! kompozytora i pozwalają klikać.
8 +
9 +use std::collections::HashMap;
10 +use std::sync::{Arc, Mutex};
11 +
12 +use wayland_client::globals::{registry_queue_init, GlobalListContents};
13 +use wayland_client::protocol::wl_registry;
14 +use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, WEnum};
15 +use wayland_protocols::ext::workspace::v1::client::{
16 + ext_workspace_group_handle_v1 as group, ext_workspace_handle_v1 as ws,
17 + ext_workspace_manager_v1 as mgr,
18 +};
19 +
20 +/// Migawka przestrzeni roboczej dla UI.
21 +#[derive(Debug, Clone, Default)]
22 +pub struct Workspace {
23 + pub id: String,
24 + pub name: String,
25 + pub active: bool,
26 +}
27 +
28 +#[derive(Default)]
29 +struct Entry {
30 + id: String,
31 + name: String,
32 + active: bool,
33 + proxy: Option<ws::ExtWorkspaceHandleV1>,
34 + removed: bool,
35 +}
36 +
37 +#[derive(Default)]
38 +struct Shared {
39 + order: Vec<u64>,
40 + map: HashMap<u64, Entry>,
41 +}
42 +
43 +struct DispatchState {
44 + shared: Arc<Mutex<Shared>>,
45 +}
46 +
47 +impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for DispatchState {
48 + fn event(
49 + _: &mut Self,
50 + _: &wl_registry::WlRegistry,
51 + _: wl_registry::Event,
52 + _: &GlobalListContents,
53 + _: &Connection,
54 + _: &QueueHandle<Self>,
55 + ) {
56 + }
57 +}
58 +
59 +impl Dispatch<group::ExtWorkspaceGroupHandleV1, ()> for DispatchState {
60 + fn event(
61 + _: &mut Self,
62 + _: &group::ExtWorkspaceGroupHandleV1,
63 + _: group::Event,
64 + _: &(),
65 + _: &Connection,
66 + _: &QueueHandle<Self>,
67 + ) {
68 + }
69 +}
70 +
71 +impl Dispatch<mgr::ExtWorkspaceManagerV1, ()> for DispatchState {
72 + fn event(
73 + state: &mut Self,
74 + _mgr: &mgr::ExtWorkspaceManagerV1,
75 + event: mgr::Event,
76 + _: &(),
77 + _: &Connection,
78 + _: &QueueHandle<Self>,
79 + ) {
80 + if let mgr::Event::Workspace { workspace } = event {
81 + let id = workspace.id().protocol_id() as u64;
82 + let mut s = state.shared.lock().unwrap();
83 + s.order.push(id);
84 + s.map.insert(
85 + id,
86 + Entry {
87 + proxy: Some(workspace),
88 + ..Default::default()
89 + },
90 + );
91 + }
92 + }
93 +}
94 +
95 +impl Dispatch<ws::ExtWorkspaceHandleV1, ()> for DispatchState {
96 + fn event(
97 + state: &mut Self,
98 + handle: &ws::ExtWorkspaceHandleV1,
99 + event: ws::Event,
100 + _: &(),
101 + _: &Connection,
102 + _: &QueueHandle<Self>,
103 + ) {
104 + let id = handle.id().protocol_id() as u64;
105 + let mut s = state.shared.lock().unwrap();
106 + let Some(e) = s.map.get_mut(&id) else {
107 + return;
108 + };
109 + match event {
110 + ws::Event::Id { id } => e.id = id,
111 + ws::Event::Name { name } => e.name = name,
112 + ws::Event::State { state } => {
113 + e.active = matches!(state, WEnum::Value(ws::State::Active));
114 + }
115 + ws::Event::Removed => e.removed = true,
116 + _ => {}
117 + }
118 + }
119 +}
120 +
121 +/// Powłoka połączona z listą przestrzeni roboczych kompozytora.
122 +pub struct Workspaces {
123 + shared: Arc<Mutex<Shared>>,
124 + conn: Connection,
125 +}
126 +
127 +impl Workspaces {
128 + /// Łączy się z kompozytorem i uruchamia wątek śledzący przestrzenie.
129 + /// `None`, gdy nie ma kompozytora albo brakuje protokołu.
130 + pub fn spawn() -> Option<Self> {
131 + let _ = libluna_core::runtime::ensure_runtime_dir();
132 + let conn = Connection::connect_to_env().ok()?;
133 + let (globals, mut queue) = registry_queue_init::<DispatchState>(&conn).ok()?;
134 + let qh = queue.handle();
135 +
136 + let shared = Arc::new(Mutex::new(Shared::default()));
137 + let mut state = DispatchState {
138 + shared: Arc::clone(&shared),
139 + };
140 +
141 + let _manager = globals
142 + .bind::<mgr::ExtWorkspaceManagerV1, DispatchState, ()>(&qh, 1..=1, ())
143 + .ok()?;
144 +
145 + std::thread::spawn(move || loop {
146 + if queue.blocking_dispatch(&mut state).is_err() {
147 + break;
148 + }
149 + });
150 +
151 + Some(Self { shared, conn })
152 + }
153 +
154 + /// Bieżąca lista przestrzeni (kolejność jak w kompozytorze).
155 + pub fn list(&self) -> Vec<Workspace> {
156 + let s = self.shared.lock().unwrap();
157 + s.order
158 + .iter()
159 + .filter_map(|id| s.map.get(id))
160 + .filter(|e| !e.removed)
161 + .map(|e| Workspace {
162 + id: e.id.clone(),
163 + name: e.name.clone(),
164 + active: e.active,
165 + })
166 + .collect()
167 + }
168 +
169 + /// Prosi kompozytor o aktywowanie przestrzeni o indeksie `index`.
170 + pub fn activate(&self, index: usize) {
171 + {
172 + let s = self.shared.lock().unwrap();
173 + if let Some(id) = s.order.get(index) {
174 + if let Some(h) = s.map.get(id).and_then(|e| e.proxy.as_ref()) {
175 + let _ = h.activate();
176 + }
177 + }
178 + }
179 + let _ = self.conn.flush();
180 + }
181 +}
@@ -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", "unstable"] }
21 +wayland-protocols = { version = "0.32", features = ["server", "unstable", "staging"] }
22 22 wayland-protocols-wlr = { version = "0.3", features = ["server"] }
23 23 rustix = { version = "0.38", features = ["event", "fs", "time", "mm"] }
@@ -324,6 +324,7 @@ fn upsert_window(
324 324 image: std::sync::Arc<ImageData>,
325 325 ) -> (usize, bool) {
326 326 let mut slot = data.slot.lock_or_recover();
327 + let ws = *state.ws_current.lock_or_recover();
327 328 let mut windows = state.windows.lock_or_recover();
328 329 let mut created = false;
329 330 let idx = match *slot {
@@ -359,6 +360,7 @@ fn upsert_window(
359 360 popup,
360 361 layer,
361 362 mapped: true,
363 + workspace: ws,
362 364 minimized: false,
363 365 maximized: false,
364 366 fullscreen: false,
@@ -322,6 +322,7 @@ mod tests {
322 322 popup: false,
323 323 layer: false,
324 324 mapped: true,
325 + workspace: 0,
325 326 minimized: true,
326 327 maximized: true,
327 328 fullscreen: false,
@@ -20,6 +20,7 @@ mod shell;
20 20 mod shm;
21 21 mod state;
22 22 mod sync;
23 +mod workspace;
23 24
24 25 use std::error::Error;
25 26 use std::os::fd::{AsRawFd, BorrowedFd};
@@ -27,6 +28,7 @@ use std::panic::{catch_unwind, AssertUnwindSafe};
27 28 use std::sync::Arc;
28 29
29 30 use libluna_core::Event;
31 +use wayland_protocols::ext::workspace::v1::server::ext_workspace_manager_v1::ExtWorkspaceManagerV1;
30 32 use wayland_protocols::xdg::decoration::zv1::server::zxdg_decoration_manager_v1;
31 33 use wayland_protocols::xdg::shell::server::xdg_wm_base;
32 34 use wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
@@ -89,6 +91,7 @@ fn main() -> Result<(), Box<dyn Error>> {
89 91 handle.create_global::<State, wl_data_device_manager::WlDataDeviceManager, ()>(3, ());
90 92 handle.create_global::<State, wl_seat::WlSeat, ()>(1, ());
91 93 handle.create_global::<State, ZwlrForeignToplevelManagerV1, ()>(3, ());
94 + handle.create_global::<State, ExtWorkspaceManagerV1, ()>(1, ());
92 95
93 96 // Gniazdo nasłuchu: `wayland-0`, `wayland-1`, …
94 97 ensure_runtime_dir()?;
@@ -108,13 +111,18 @@ fn main() -> Result<(), Box<dyn Error>> {
108 111
109 112 // Wyjście: realne DRM/KMS (gdy `SELENE_BACKEND=drm`) lub nested na nadrzędnym kompozytorze.
110 113 if std::env::var("SELENE_BACKEND").as_deref() == Ok("drm") {
111 - output::spawn_drm(Arc::clone(&state.windows), Arc::clone(&state.cursor));
114 + output::spawn_drm(
115 + Arc::clone(&state.windows),
116 + Arc::clone(&state.cursor),
117 + Arc::clone(&state.ws_current),
118 + );
112 119 } else {
113 120 output::spawn(
114 121 Arc::clone(&state.windows),
115 122 Arc::clone(&state.outputs),
116 123 Arc::clone(&events),
117 124 wake_write,
125 + Arc::clone(&state.ws_current),
118 126 1280,
119 127 800,
120 128 );
@@ -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};
18 +use crate::state::{CursorModel, Window, WindowModel, WorkspaceModel};
19 19 use crate::sync::LockExt;
20 20
21 21 /// Uruchamia wątek wyjścia (nested Wayland) o zadanym rozmiarze.
@@ -24,6 +24,7 @@ pub fn spawn(
24 24 outputs: Outputs,
25 25 events: Events,
26 26 wake: UnixStream,
27 + ws: WorkspaceModel,
27 28 width: u32,
28 29 height: u32,
29 30 ) {
@@ -46,7 +47,8 @@ pub fn spawn(
46 47 input::push(&events, &mut wake, frame.events);
47 48 // Narysuj scenę.
48 49 let size = frame.size;
49 - build_scene(frame.scene, size, 0.0, 0.0, &model, None);
50 + let cur_ws = *ws.lock_or_recover();
51 + build_scene(frame.scene, size, 0.0, 0.0, &model, cur_ws, None);
50 52 // Utrzymuj klatki — model okien zmienia się poza tą pętlą.
51 53 frame.request_frame();
52 54 });
@@ -89,7 +91,7 @@ fn open_drm_waiting() -> Result<DrmBackend, DrmError> {
89 91 ///
90 92 /// Wymaga bycia DRM masterem; jeśli inną sesję trzyma master, kończy bez efektu
91 93 /// (wywołujący powinien wtedy użyć wyjścia nested).
92 -pub fn spawn_drm(model: WindowModel, cursor: CursorModel) {
94 +pub fn spawn_drm(model: WindowModel, cursor: CursorModel, ws: WorkspaceModel) {
93 95 std::thread::spawn(move || {
94 96 let mut backend = match open_drm_waiting() {
95 97 Ok(backend) => backend,
@@ -168,10 +170,11 @@ pub fn spawn_drm(model: WindowModel, cursor: CursorModel) {
168 170 let mut last_cur: Option<Point> = None;
169 171 loop {
170 172 let cur = *cursor.lock_or_recover();
173 + let cur_ws = *ws.lock_or_recover();
171 174
172 175 // Sygnatura sceny nie zależy od kursora, gdy rysuje go sprzęt — dzięki temu
173 176 // samo przesunięcie myszy nie powoduje przemalowania sceny.
174 - let sig = scene_signature(&model, if hw_cursor { None } else { cur });
177 + let sig = scene_signature(&model, cur_ws, if hw_cursor { None } else { cur });
175 178 let scene_changed = last_sig != Some(sig);
176 179 let cur_changed = last_cur != cur;
177 180 if !scene_changed && !cur_changed {
@@ -221,6 +224,7 @@ pub fn spawn_drm(model: WindowModel, cursor: CursorModel) {
221 224 ox,
222 225 oy,
223 226 &model,
227 + cur_ws,
224 228 if hw_cursor { None } else { cur },
225 229 );
226 230 renderers[i].draw(&scene, 1.0, bg);
@@ -278,13 +282,14 @@ fn cursor_image() -> (Vec<u8>, u32, u32) {
278 282 (renderer.data().to_vec(), w, h)
279 283 }
280 284
281 -/// Sygnatura sceny (okna + kursor) — do wykrywania, że nic się nie zmieniło.
282 -fn scene_signature(model: &WindowModel, cursor: Option<Point>) -> u64 {
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 {
283 287 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
284 288 let mut mix = |v: u64| {
285 289 h ^= v;
286 290 h = h.wrapping_mul(0x0000_0100_0000_01b3);
287 291 };
292 + mix(current_ws as u64);
288 293 {
289 294 let windows = model.lock_or_recover();
290 295 mix(windows.len() as u64);
@@ -312,6 +317,7 @@ fn build_scene(
312 317 ox: f64,
313 318 oy: f64,
314 319 model: &WindowModel,
320 + current_ws: usize,
315 321 cursor: Option<Point>,
316 322 ) {
317 323 let sh = |r: Rect| r.offset(-ox, -oy);
@@ -326,7 +332,7 @@ fn build_scene(
326 332 let mut order: Vec<&Window> = windows.iter().collect();
327 333 order.sort_by_key(|w| w.z);
328 334 for w in order {
329 - if !w.mapped || w.minimized {
335 + if !w.mapped || w.minimized || !w.in_workspace(current_ws) {
330 336 continue;
331 337 }
332 338 // Tło treści tylko dla zwykłych okien — powierzchnie warstwowe (panele)
@@ -457,6 +463,7 @@ mod tests {
457 463 0.0,
458 464 0.0,
459 465 &model,
466 + 0,
460 467 Some(Point::new(100.0, 100.0)),
461 468 );
462 469 renderer.draw(&scene, 1.0, bg);
@@ -487,6 +494,7 @@ mod tests {
487 494 popup: false,
488 495 layer: true,
489 496 mapped: true,
497 + workspace: 0,
490 498 minimized: false,
491 499 maximized: false,
492 500 fullscreen: false,
@@ -507,6 +515,7 @@ mod tests {
507 515 0.0,
508 516 0.0,
509 517 &model,
518 + 0,
510 519 Some(Point::new(100.0, 100.0)),
511 520 );
512 521 renderer.draw(&scene, 1.0, bg);
@@ -181,9 +181,10 @@ pub fn handle_input(state: &mut State, handle: &DisplayHandle, event: &Event) {
181 181 /// Trafienie w okno (najwyższe wg `z`) i w którą jego część.
182 182 fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
183 183 let windows = state.windows.lock_or_recover();
184 + let cur_ws = *state.ws_current.lock_or_recover();
184 185 let mut best: Option<(usize, i32)> = None;
185 186 for (i, w) in windows.iter().enumerate() {
186 - if w.mapped && !w.minimized && w.outer().contains(pos) {
187 + if w.mapped && !w.minimized && w.in_workspace(cur_ws) && w.outer().contains(pos) {
187 188 match best {
188 189 Some((_, z)) if z >= w.z => {}
189 190 _ => best = Some((i, w.z)),
@@ -214,9 +215,10 @@ fn hit(state: &State, pos: Point) -> Option<(usize, Part)> {
214 215 /// Najwyższe okno, którego **treść** zawiera punkt (do zdarzeń klienta).
215 216 fn content_window(state: &State, pos: Point) -> Option<usize> {
216 217 let windows = state.windows.lock_or_recover();
218 + let cur_ws = *state.ws_current.lock_or_recover();
217 219 let mut best: Option<(usize, i32)> = None;
218 220 for (i, w) in windows.iter().enumerate() {
219 - if w.mapped && !w.minimized && w.content().contains(pos) {
221 + if w.mapped && !w.minimized && w.in_workspace(cur_ws) && w.content().contains(pos) {
220 222 match best {
221 223 Some((_, z)) if z >= w.z => {}
222 224 _ => best = Some((i, w.z)),
@@ -799,7 +801,7 @@ fn window_shortcut(state: &mut State, keysym: u32, st: ButtonState) -> bool {
799 801 // Zjedz także zwolnienia naszych klawiszy, by nie trafiły do klienta.
800 802 return matches!(
801 803 keysym,
802 - 0x006d | 0x0066 | 0x006c | 0x004c | 0xff52 | 0xff54 | 0xff09
804 + 0x006d | 0x0066 | 0x006c | 0x004c | 0xff52 | 0xff54 | 0xff09 | 0x0031..=0x0038
803 805 );
804 806 }
805 807 let focused = state.keyboard_focus.as_ref().map(|(s, _)| *s);
@@ -843,6 +845,18 @@ fn window_shortcut(state: &mut State, keysym: u32, st: ButtonState) -> bool {
843 845 cycle_windows(state);
844 846 true
845 847 }
848 + // Super+1..8 — przełącz przestrzeń; Super+Shift+1..8 — przenieś okno.
849 + 0x0031..=0x0038 => {
850 + let ws = (keysym - 0x0031) as usize;
851 + if state.sent_mods.shift {
852 + if let Some(s) = focused {
853 + move_window_to_workspace(state, s, ws);
854 + }
855 + } else {
856 + switch_workspace(state, ws);
857 + }
858 + true
859 + }
846 860 _ => false,
847 861 }
848 862 }
@@ -890,6 +904,67 @@ fn cycle_windows(state: &mut State) {
890 904 raise_and_focus(state, next);
891 905 }
892 906
907 +/// Najwyższe zwykłe okno w danej przestrzeni roboczej.
908 +fn top_window_in_ws(state: &State, ws: usize) -> Option<usize> {
909 + let windows = state.windows.lock_or_recover();
910 + windows
911 + .iter()
912 + .enumerate()
913 + .filter(|(_, w)| w.mapped && !w.minimized && !w.popup && !w.layer && w.workspace == ws)
914 + .max_by_key(|(_, w)| w.z)
915 + .map(|(i, _)| i)
916 +}
917 +
918 +/// Przełącza bieżącą przestrzeń roboczą, przenosząc fokus na jej wierzchołek.
919 +pub fn switch_workspace(state: &mut State, ws: usize) {
920 + let ws = ws.min(state.ws_count.saturating_sub(1));
921 + {
922 + let mut cur = state.ws_current.lock_or_recover();
923 + if *cur == ws {
924 + return;
925 + }
926 + *cur = ws;
927 + }
928 + // Zdejmij fokus z okna, którego nie ma już na ekranie.
929 + let focused = state.keyboard_focus.as_ref().map(|(s, _)| *s);
930 + let keep = focused
931 + .and_then(|s| {
932 + state
933 + .windows
934 + .lock_or_recover()
935 + .get(s)
936 + .map(|w| w.in_workspace(ws))
937 + })
938 + .unwrap_or(false);
939 + if !keep {
940 + set_keyboard_focus(state, usize::MAX);
941 + if let Some(top) = top_window_in_ws(state, ws) {
942 + raise_and_focus(state, top);
943 + }
944 + }
945 + crate::workspace::broadcast_workspace_states(state);
946 +}
947 +
948 +/// Przenosi okno do zadanej przestrzeni roboczej (skrót Super+Shift+cyfra).
949 +fn move_window_to_workspace(state: &mut State, slot: usize, ws: usize) {
950 + let ws = ws.min(state.ws_count.saturating_sub(1));
951 + {
952 + let mut windows = state.windows.lock_or_recover();
953 + match windows.get_mut(slot) {
954 + Some(w) if !w.layer && !w.popup => w.workspace = ws,
955 + _ => return,
956 + }
957 + }
958 + let cur = *state.ws_current.lock_or_recover();
959 + if ws != cur && state.keyboard_focus.as_ref().map(|(s, _)| *s) == Some(slot) {
960 + set_keyboard_focus(state, usize::MAX);
961 + if let Some(top) = top_window_in_ws(state, cur) {
962 + raise_and_focus(state, top);
963 + }
964 + }
965 + crate::workspace::broadcast_workspace_states(state);
966 +}
967 +
893 968 /// Wysyła wciśnięcia/zwolnienia klawiszy modyfikujących wynikające z bieżącego stanu.
894 969 fn sync_modifiers(state: &mut State, kb: &wl_keyboard::WlKeyboard, keysym: u32, st: ButtonState) {
895 970 let mut mods = state.sent_mods;
@@ -1076,6 +1151,7 @@ mod tests {
1076 1151 popup: false,
1077 1152 layer: false,
1078 1153 mapped: true,
1154 + workspace: 0,
1079 1155 minimized: false,
1080 1156 maximized: false,
1081 1157 fullscreen: false,
@@ -1084,6 +1160,35 @@ mod tests {
1084 1160 }
1085 1161 }
1086 1162
1163 + #[test]
1164 + fn workspace_predicate_switch_and_move() {
1165 + let mut s = State::default();
1166 + let mut a = sample_window();
1167 + a.workspace = 0;
1168 + let mut b = sample_window();
1169 + b.workspace = 1;
1170 + {
1171 + let mut w = s.windows.lock_or_recover();
1172 + w.push(a);
1173 + w.push(b);
1174 + }
1175 + assert!(s.windows.lock_or_recover()[0].in_workspace(0));
1176 + assert!(!s.windows.lock_or_recover()[0].in_workspace(1));
1177 + // Powierzchnia warstwowa (panel) jest widoczna w każdej przestrzeni.
1178 + let mut panel = sample_window();
1179 + panel.layer = true;
1180 + panel.workspace = 3;
1181 + assert!(panel.in_workspace(0));
1182 +
1183 + switch_workspace(&mut s, 1);
1184 + assert_eq!(*s.ws_current.lock_or_recover(), 1);
1185 + move_window_to_workspace(&mut s, 0, 2);
1186 + assert_eq!(s.windows.lock_or_recover()[0].workspace, 2);
1187 + // Numer przestrzeni jest klamrowany do zakresu.
1188 + switch_workspace(&mut s, 99);
1189 + assert_eq!(*s.ws_current.lock_or_recover(), s.ws_count - 1);
1190 + }
1191 +
1087 1192 #[test]
1088 1193 fn window_controls_are_laid_out() {
1089 1194 let w = sample_window();
@@ -5,6 +5,10 @@ use std::time::Instant;
5 5
6 6 use libluna_core::{Modifiers, Point, Rect};
7 7 use libluna_render::ImageData;
8 +use wayland_protocols::ext::workspace::v1::server::{
9 + ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
10 + ext_workspace_handle_v1::ExtWorkspaceHandleV1, ext_workspace_manager_v1::ExtWorkspaceManagerV1,
11 +};
8 12 use wayland_protocols_wlr::foreign_toplevel::v1::server::zwlr_foreign_toplevel_manager_v1::ZwlrForeignToplevelManagerV1;
9 13 use wayland_server::protocol::wl_data_device::WlDataDevice;
10 14 use wayland_server::protocol::wl_data_source::WlDataSource;
@@ -48,6 +52,8 @@ pub struct Window {
48 52 pub layer: bool,
49 53 /// Czy powierzchnia jest widoczna (popup znika po `popup_done`).
50 54 pub mapped: bool,
55 + /// Przestrzeń robocza, do której należy okno (panele/popupy są globalne).
56 + pub workspace: usize,
51 57 /// Zminimalizowane (ukryte przez kompozytor; przywracane skrótem).
52 58 pub minimized: bool,
53 59 /// Zmaksymalizowane do obszaru monitora.
@@ -61,6 +67,14 @@ pub struct Window {
61 67 }
62 68
63 69 impl Window {
70 + /// Czy okno pokazuje się w danej przestrzeni roboczej.
71 + ///
72 + /// Powierzchnie warstwowe (pasek, tło, OSD) są globalne — należą do każdej
73 + /// przestrzeni; zwykłe okna i popupy widoczne są tylko w swojej.
74 + pub fn in_workspace(&self, ws: usize) -> bool {
75 + self.layer || self.workspace == ws
76 + }
77 +
64 78 /// Wysokość dekoracji (zero dla popupów, paneli i pełnego ekranu).
65 79 fn bar(&self) -> f64 {
66 80 if self.popup || self.layer || self.fullscreen {
@@ -156,6 +170,9 @@ pub type WindowModel = Arc<Mutex<Vec<Window>>>;
156 170 /// Współdzielona pozycja kursora (tryb DRM/evdev); `None` = nie rysujemy kursora.
157 171 pub type CursorModel = Arc<Mutex<Option<Point>>>;
158 172
173 +/// Współdzielony numer bieżącej przestrzeni roboczej (kompozytor ↔ wątek wyjścia).
174 +pub type WorkspaceModel = Arc<Mutex<usize>>;
175 +
159 176 /// Trwające przeciąganie okna.
160 177 #[derive(Debug, Clone, Copy)]
161 178 pub struct DragState {
@@ -202,6 +219,16 @@ pub struct State {
202 219 /// Podłączeni menedżerowie `zwlr_foreign_toplevel` (paski/taskbary).
203 220 pub toplevel_managers: Vec<ZwlrForeignToplevelManagerV1>,
204 221
222 + // — Przestrzenie robocze (`ext_workspace_v1`) —
223 + /// Liczba przestrzeni roboczych.
224 + pub ws_count: usize,
225 + /// Bieżąca przestrzeń robocza (współdzielona z wątkiem wyjścia).
226 + pub ws_current: WorkspaceModel,
227 + /// Podłączeni menedżerowie `ext_workspace` oraz ich grupy i uchwyty przestrzeni.
228 + pub ws_managers: Vec<ExtWorkspaceManagerV1>,
229 + pub ws_groups: Vec<ExtWorkspaceGroupHandleV1>,
230 + pub ws_handles: Vec<ExtWorkspaceHandleV1>,
231 +
205 232 // — Wejście —
206 233 pub pointer: Option<WlPointer>,
207 234 pub keyboard: Option<WlKeyboard>,
@@ -237,6 +264,11 @@ impl Default for State {
237 264 z_counter: 1,
238 265 outputs: Arc::new(Mutex::new(vec![OutputInfo::default()])),
239 266 toplevel_managers: Vec::new(),
267 + ws_count: 4,
268 + ws_current: Arc::new(Mutex::new(0)),
269 + ws_managers: Vec::new(),
270 + ws_groups: Vec::new(),
271 + ws_handles: Vec::new(),
240 272 pointer: None,
241 273 keyboard: None,
242 274 pointer_focus: None,
243 275 new file mode 100644
@@ -0,0 +1,200 @@
1 +//! `ext_workspace_v1` — lista i przełączanie przestrzeni roboczych dla powłoki.
2 +//!
3 +//! Kompozytor wystawia globalny menedżer; każdy klient (pasek `luna-shell`) dostaje
4 +//! jedną **grupę** i po jednym **uchwycie** na przestrzeń roboczą. Uchwyt niesie
5 +//! `id`/`name`/`state` (m.in. `active`), a jego żądanie `activate` przełącza przestrzeń
6 +//! w kompozytorze (patrz [`crate::seat::switch_workspace`]) — to czyni kropki w pasku
7 +//! w pełni funkcjonalnymi.
8 +
9 +use wayland_protocols::ext::workspace::v1::server::{
10 + ext_workspace_group_handle_v1 as group, ext_workspace_handle_v1 as workspace,
11 + ext_workspace_manager_v1 as manager,
12 +};
13 +use wayland_server::{
14 + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum,
15 +};
16 +
17 +use wayland_protocols::ext::workspace::v1::server::ext_workspace_group_handle_v1::GroupCapabilities;
18 +use wayland_protocols::ext::workspace::v1::server::ext_workspace_handle_v1::{
19 + State as WsState, WorkspaceCapabilities,
20 +};
21 +
22 +use crate::state::State;
23 +use crate::sync::LockExt;
24 +
25 +impl GlobalDispatch<manager::ExtWorkspaceManagerV1, ()> for State {
26 + fn bind(
27 + state: &mut Self,
28 + handle: &DisplayHandle,
29 + _client: &Client,
30 + resource: New<manager::ExtWorkspaceManagerV1>,
31 + _data: &(),
32 + data_init: &mut DataInit<'_, Self>,
33 + ) {
34 + let mgr = data_init.init(resource, ());
35 + announce(state, handle, &mgr);
36 + state.ws_managers.push(mgr);
37 + }
38 +}
39 +
40 +impl Dispatch<manager::ExtWorkspaceManagerV1, ()> for State {
41 + fn request(
42 + state: &mut Self,
43 + _client: &Client,
44 + resource: &manager::ExtWorkspaceManagerV1,
45 + request: manager::Request,
46 + _data: &(),
47 + _handle: &DisplayHandle,
48 + _data_init: &mut DataInit<'_, Self>,
49 + ) {
50 + match request {
51 + // Zmiany wysyłamy od razu, więc `commit` nic nie robi.
52 + manager::Request::Commit => {}
53 + manager::Request::Stop => {
54 + let mgr_id = resource.id();
55 + let client_id = resource.client().map(|c| c.id());
56 + state.ws_managers.retain(|m| m.id() != mgr_id);
57 + if let Some(client_id) = client_id {
58 + state
59 + .ws_groups
60 + .retain(|g| g.client().map(|c| c.id() != client_id).unwrap_or(false));
61 + state
62 + .ws_handles
63 + .retain(|h| h.client().map(|c| c.id() != client_id).unwrap_or(false));
64 + }
65 + }
66 + _ => {}
67 + }
68 + }
69 +}
70 +
71 +impl Dispatch<group::ExtWorkspaceGroupHandleV1, ()> for State {
72 + fn request(
73 + state: &mut Self,
74 + _client: &Client,
75 + resource: &group::ExtWorkspaceGroupHandleV1,
76 + request: group::Request,
77 + _data: &(),
78 + _handle: &DisplayHandle,
79 + _data_init: &mut DataInit<'_, Self>,
80 + ) {
81 + match request {
82 + // Tworzenie przestrzeni przez klienta nie jest wspierane (stały zestaw).
83 + group::Request::CreateWorkspace { .. } => {}
84 + group::Request::Destroy => {
85 + let id = resource.id();
86 + state.ws_groups.retain(|g| g.id() != id);
87 + }
88 + _ => {}
89 + }
90 + }
91 +}
92 +
93 +impl Dispatch<workspace::ExtWorkspaceHandleV1, usize> for State {
94 + fn request(
95 + state: &mut Self,
96 + _client: &Client,
97 + resource: &workspace::ExtWorkspaceHandleV1,
98 + request: workspace::Request,
99 + index: &usize,
100 + _handle: &DisplayHandle,
101 + _data_init: &mut DataInit<'_, Self>,
102 + ) {
103 + match request {
104 + workspace::Request::Activate => crate::seat::switch_workspace(state, *index),
105 + // Dezaktywacja/przypisanie/usuwanie: poza zakresem (stały zestaw przestrzeni).
106 + workspace::Request::Deactivate
107 + | workspace::Request::Assign { .. }
108 + | workspace::Request::Remove => {}
109 + workspace::Request::Destroy => {
110 + let id = Resource::id(resource);
111 + state.ws_handles.retain(|h| Resource::id(h) != id);
112 + }
113 + _ => {}
114 + }
115 + }
116 +}
117 +
118 +/// Zgłasza nowo podłączonemu menedżerowi grupę i wszystkie przestrzenie.
119 +fn announce(state: &mut State, display: &DisplayHandle, mgr: &manager::ExtWorkspaceManagerV1) {
120 + let Some(client) = mgr.client() else {
121 + return;
122 + };
123 + let Ok(group) =
124 + client.create_resource::<group::ExtWorkspaceGroupHandleV1, (), State>(display, 1, ())
125 + else {
126 + return;
127 + };
128 + let _ = mgr.send_event(manager::Event::WorkspaceGroup {
129 + workspace_group: group.clone(),
130 + });
131 + let _ = group.send_event(group::Event::Capabilities {
132 + capabilities: WEnum::<GroupCapabilities>::Unknown(0),
133 + });
134 +
135 + let cur = *state.ws_current.lock_or_recover();
136 + for index in 0..state.ws_count {
137 + let Ok(ws) = client
138 + .create_resource::<workspace::ExtWorkspaceHandleV1, usize, State>(display, 1, index)
139 + else {
140 + continue;
141 + };
142 + let _ = mgr.send_event(manager::Event::Workspace {
143 + workspace: ws.clone(),
144 + });
145 + let _ = ws.send_event(workspace::Event::Id {
146 + id: format!("ws{}", index + 1),
147 + });
148 + let _ = ws.send_event(workspace::Event::Name {
149 + name: format!("{}", index + 1),
150 + });
151 + let _ = ws.send_event(workspace::Event::Coordinates {
152 + coordinates: Vec::new(),
153 + });
154 + let _ = ws.send_event(workspace::Event::Capabilities {
155 + capabilities: WEnum::Value(WorkspaceCapabilities::Activate),
156 + });
157 + let _ = ws.send_event(workspace::Event::State {
158 + state: ws_state(index, cur),
159 + });
160 + let _ = group.send_event(group::Event::WorkspaceEnter {
161 + workspace: ws.clone(),
162 + });
163 + state.ws_handles.push(ws);
164 + }
165 + state.ws_groups.push(group);
166 + let _ = mgr.send_event(manager::Event::Done);
167 +}
168 +
169 +/// Rozgłasza stan wszystkich przestrzeni (po przełączeniu) do podłączonych klientów.
170 +pub fn broadcast_workspace_states(state: &State) {
171 + let cur = *state.ws_current.lock_or_recover();
172 + for h in &state.ws_handles {
173 + if let Some(&index) = h.data::<usize>() {
174 + let _ = h.send_event(workspace::Event::State {
175 + state: ws_state(index, cur),
176 + });
177 + }
178 + }
179 +}
180 +
181 +/// Stan przestrzeni `index`, gdy bieżąca to `cur`.
182 +fn ws_state(index: usize, cur: usize) -> WEnum<WsState> {
183 + if index == cur {
184 + WEnum::Value(WsState::Active)
185 + } else {
186 + WEnum::Unknown(0)
187 + }
188 +}
189 +
190 +#[cfg(test)]
191 +mod tests {
192 + use super::*;
193 +
194 + #[test]
195 + fn only_the_current_workspace_is_active() {
196 + assert!(matches!(ws_state(0, 0), WEnum::Value(WsState::Active)));
197 + assert!(matches!(ws_state(1, 0), WEnum::Unknown(0)));
198 + assert!(matches!(ws_state(3, 3), WEnum::Value(WsState::Active)));
199 + }
200 +}