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
+}