1use smithay::output::Output;
31use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
32use smithay::reexports::wayland_server::{
33 Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
34 backend::{ClientId, GlobalId},
35};
36use wayland_protocols::ext::workspace::v1::server::{
37 ext_workspace_group_handle_v1::{self, ExtWorkspaceGroupHandleV1, GroupCapabilities},
38 ext_workspace_handle_v1::{self, ExtWorkspaceHandleV1, State, WorkspaceCapabilities},
39 ext_workspace_manager_v1::{self, ExtWorkspaceManagerV1},
40};
41
42use crate::wayland::frontend::AppState;
43use crate::wayland::wm;
44
45#[derive(Clone, Debug)]
48pub struct HandleData {
49 manager: ExtWorkspaceManagerV1,
50 index: u32,
51}
52
53struct Manager {
56 manager: ExtWorkspaceManagerV1,
57 group: ExtWorkspaceGroupHandleV1,
58 handles: Vec<ExtWorkspaceHandleV1>,
59 pending: Option<u32>,
60}
61
62pub struct WorkspaceState {
64 managers: Vec<Manager>,
65 count: u32,
66 current: u32,
67 outputs: Vec<(u32, Output)>,
70 global: Option<GlobalId>,
71}
72
73impl WorkspaceState {
74 pub fn new(dh: &DisplayHandle) -> WorkspaceState {
76 let global = dh.create_global::<AppState, ExtWorkspaceManagerV1, _>(1, ());
77 WorkspaceState {
78 managers: Vec::new(),
79 count: 1,
80 current: 0,
81 outputs: Vec::new(),
82 global: Some(global),
83 }
84 }
85}
86
87fn bounded(count: u32, current: u32) -> (u32, u32) {
89 let count = count.max(1);
90 (count, current.min(count - 1))
91}
92
93fn coordinates(index: u32) -> Vec<u8> {
96 index.to_ne_bytes().to_vec()
97}
98
99fn state_bits(index: u32, current: u32) -> State {
101 if index == current {
102 State::Active
103 } else {
104 State::empty()
105 }
106}
107
108fn active_changes(count: u32, from: u32, to: u32) -> Vec<(u32, State)> {
110 (0..count)
111 .filter(|index| state_bits(*index, from) != state_bits(*index, to))
112 .map(|index| (index, state_bits(index, to)))
113 .collect()
114}
115
116fn spanned_outputs(state: &AppState) -> Vec<(u32, Output)> {
118 state
119 .output_nodes
120 .iter()
121 .filter(|n| n.owner.is_none() && n.global.is_some())
122 .map(|n| (n.id, n.output.clone()))
123 .collect()
124}
125
126fn send_workspace(
129 state: &AppState,
130 entry: &Manager,
131 index: u32,
132 current: u32,
133 client: &Client,
134) -> Option<ExtWorkspaceHandleV1> {
135 let handle = client
136 .create_resource::<ExtWorkspaceHandleV1, HandleData, AppState>(
137 &state.dh,
138 entry.manager.version(),
139 HandleData {
140 manager: entry.manager.clone(),
141 index,
142 },
143 )
144 .ok()?;
145 entry.manager.workspace(&handle);
146 let name = (index + 1).to_string();
147 handle.id(name.clone());
148 handle.name(name);
149 handle.coordinates(coordinates(index));
150 handle.capabilities(WorkspaceCapabilities::Activate);
151 handle.state(state_bits(index, current));
152 entry.group.workspace_enter(&handle);
153 Some(handle)
154}
155
156pub fn set_desktops(state: &mut AppState, count: u32, current: u32) {
159 let (count, current) = bounded(count, current);
160 let previous = state.workspace.current;
161 if (count, current) == (state.workspace.count, previous) {
162 return;
163 }
164 state.workspace.count = count;
165 state.workspace.current = current;
166 let mut managers = std::mem::take(&mut state.workspace.managers);
167 for entry in &mut managers {
168 let Some(client) = entry.manager.client() else {
169 continue;
170 };
171 while entry.handles.len() as u32 > count {
172 if let Some(handle) = entry.handles.pop() {
173 entry.group.workspace_leave(&handle);
174 handle.removed();
175 }
176 }
177 let kept = entry.handles.len() as u32;
178 for (index, bits) in active_changes(kept, previous, current) {
179 entry.handles[index as usize].state(bits);
180 }
181 while (entry.handles.len() as u32) < count {
182 let index = entry.handles.len() as u32;
183 match send_workspace(state, entry, index, current, &client) {
184 Some(handle) => entry.handles.push(handle),
185 None => break,
186 }
187 }
188 entry.manager.done();
189 }
190 state.workspace.managers = managers;
191}
192
193pub fn outputs_changed(state: &mut AppState) {
195 let outputs = spanned_outputs(state);
196 let same = outputs.len() == state.workspace.outputs.len()
197 && outputs
198 .iter()
199 .zip(&state.workspace.outputs)
200 .all(|((a, _), (b, _))| a == b);
201 if same {
202 return;
203 }
204 let previous = std::mem::replace(&mut state.workspace.outputs, outputs);
205 for entry in &state.workspace.managers {
206 let Some(client) = entry.manager.client() else {
207 continue;
208 };
209 for (id, output) in &previous {
210 if !state.workspace.outputs.iter().any(|(i, _)| i == id) {
211 for wl_output in output.client_outputs(&client) {
212 entry.group.output_leave(&wl_output);
213 }
214 }
215 }
216 for (id, output) in &state.workspace.outputs {
217 if !previous.iter().any(|(i, _)| i == id) {
218 for wl_output in output.client_outputs(&client) {
219 entry.group.output_enter(&wl_output);
220 }
221 }
222 }
223 entry.manager.done();
224 }
225}
226
227pub fn output_bound(state: &mut AppState, output: &Output, wl_output: &WlOutput) {
230 if !state.workspace.outputs.iter().any(|(_, o)| o == output) {
231 return;
232 }
233 let client = wl_output.client().map(|c| c.id());
234 for entry in &state.workspace.managers {
235 if entry.manager.client().map(|c| c.id()) != client {
236 continue;
237 }
238 entry.group.output_enter(wl_output);
239 entry.manager.done();
240 }
241}
242
243impl GlobalDispatch<ExtWorkspaceManagerV1, ()> for AppState {
244 fn bind(
245 state: &mut Self,
246 _dh: &DisplayHandle,
247 client: &Client,
248 resource: New<ExtWorkspaceManagerV1>,
249 _global_data: &(),
250 data_init: &mut DataInit<'_, Self>,
251 ) {
252 let manager = data_init.init(resource, ());
253 outputs_changed(state);
254 let Ok(group) = client.create_resource::<ExtWorkspaceGroupHandleV1, (), AppState>(
255 &state.dh,
256 manager.version(),
257 (),
258 ) else {
259 return;
260 };
261 manager.workspace_group(&group);
262 group.capabilities(GroupCapabilities::empty());
263 for (_, output) in &state.workspace.outputs {
264 for wl_output in output.client_outputs(client) {
265 group.output_enter(&wl_output);
266 }
267 }
268 let mut entry = Manager {
269 manager,
270 group,
271 handles: Vec::new(),
272 pending: None,
273 };
274 for index in 0..state.workspace.count {
275 match send_workspace(state, &entry, index, state.workspace.current, client) {
276 Some(handle) => entry.handles.push(handle),
277 None => break,
278 }
279 }
280 entry.manager.done();
281 state.workspace.managers.push(entry);
282 }
283}
284
285impl Dispatch<ExtWorkspaceManagerV1, ()> for AppState {
286 fn request(
287 state: &mut Self,
288 _client: &Client,
289 manager: &ExtWorkspaceManagerV1,
290 request: ext_workspace_manager_v1::Request,
291 _data: &(),
292 _dh: &DisplayHandle,
293 _data_init: &mut DataInit<'_, Self>,
294 ) {
295 match request {
296 ext_workspace_manager_v1::Request::Commit => {
297 let pending = state
298 .workspace
299 .managers
300 .iter_mut()
301 .find(|m| m.manager == *manager)
302 .and_then(|m| m.pending.take());
303 if let Some(index) = pending
304 && index < state.workspace.count
305 {
306 wm::activate_desktop(state, index);
307 }
308 }
309 ext_workspace_manager_v1::Request::Stop => {
310 manager.finished();
311 state.workspace.managers.retain(|m| m.manager != *manager);
312 }
313 _ => {}
314 }
315 }
316
317 fn destroyed(state: &mut Self, _client: ClientId, manager: &ExtWorkspaceManagerV1, _data: &()) {
318 state.workspace.managers.retain(|m| m.manager != *manager);
319 }
320}
321
322impl Dispatch<ExtWorkspaceGroupHandleV1, ()> for AppState {
323 fn request(
324 _state: &mut Self,
325 _client: &Client,
326 _group: &ExtWorkspaceGroupHandleV1,
327 _request: ext_workspace_group_handle_v1::Request,
328 _data: &(),
329 _dh: &DisplayHandle,
330 _data_init: &mut DataInit<'_, Self>,
331 ) {
332 }
333}
334
335impl Dispatch<ExtWorkspaceHandleV1, HandleData> for AppState {
336 fn request(
337 state: &mut Self,
338 _client: &Client,
339 _handle: &ExtWorkspaceHandleV1,
340 request: ext_workspace_handle_v1::Request,
341 data: &HandleData,
342 _dh: &DisplayHandle,
343 _data_init: &mut DataInit<'_, Self>,
344 ) {
345 if let ext_workspace_handle_v1::Request::Activate = request
346 && let Some(entry) = state
347 .workspace
348 .managers
349 .iter_mut()
350 .find(|m| m.manager == data.manager)
351 {
352 entry.pending = Some(data.index);
353 }
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
363 fn coordinates_are_the_index_as_one_word() {
364 assert_eq!(coordinates(0), 0u32.to_ne_bytes().to_vec());
365 assert_eq!(coordinates(3), 3u32.to_ne_bytes().to_vec());
366 assert_eq!(coordinates(3).len(), 4);
367 }
368
369 #[test]
372 fn the_active_bit_moves_with_the_current_desktop() {
373 assert_eq!(state_bits(2, 2), State::Active);
374 assert_eq!(state_bits(1, 2), State::empty());
375 assert_eq!(
376 active_changes(4, 0, 2),
377 vec![(0, State::empty()), (2, State::Active)]
378 );
379 assert!(active_changes(4, 1, 1).is_empty());
380 assert_eq!(active_changes(2, 0, 3), vec![(0, State::empty())]);
381 }
382
383 #[test]
386 fn desktops_are_bounded_to_at_least_one_with_the_current_among_them() {
387 assert_eq!(bounded(0, 0), (1, 0));
388 assert_eq!(bounded(4, 9), (4, 3));
389 assert_eq!(bounded(4, 2), (4, 2));
390 }
391}