Skip to main content

pixelflux/wayland/
workspace.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! `ext_workspace_manager_v1`: the virtual desktops a pager reads and switches.
8//!
9//! The standard protocol is served rather than an interface of this tree's own so that the
10//! desktop shell and any other pager (a panel, a dock) read the desktops the way they read
11//! them from every other compositor. The compositor keeps only what the protocol shows: how
12//! many desktops there are and which is current, as the window policy last said
13//! ([`Command::Desktops`]), one desktop and the first before it says anything. Which desktop a
14//! window is on is the policy's alone: it hides a window on another desktop through
15//! [`States::hidden`], and a taskbar learns of it through the window list, where the hidden
16//! window leaves every output while it stays listed.
17//!
18//! Every bound manager sees one workspace group spanning every output, since a desktop here
19//! covers the whole layout, and in it a workspace per desktop, named from one and placed by a
20//! single coordinate, the desktop's index. A pager's `activate` is recorded against its manager
21//! and acted on at the `commit` that ends the pager's atomic batch, as
22//! [`Event::ActivateDesktop`] to the policy; nothing is sent back then, since the policy's
23//! answer, a [`Command::Desktops`], is what moves the state every pager sees, so a switch the
24//! policy declines leaves the pager showing what the compositor shows.
25//!
26//! [`Command::Desktops`]: crate::wm::Command::Desktops
27//! [`Event::ActivateDesktop`]: crate::wm::Event::ActivateDesktop
28//! [`States::hidden`]: crate::wm::States::hidden
29
30use 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/// The user data of a workspace handle: the manager it was sent on and the desktop it stands
46/// for, counted from zero.
47#[derive(Clone, Debug)]
48pub struct HandleData {
49    manager: ExtWorkspaceManagerV1,
50    index: u32,
51}
52
53/// One bound manager: its group, a handle per desktop in desktop order, and the switch a pager
54/// asked for ahead of its commit.
55struct Manager {
56    manager: ExtWorkspaceManagerV1,
57    group: ExtWorkspaceGroupHandleV1,
58    handles: Vec<ExtWorkspaceHandleV1>,
59    pending: Option<u32>,
60}
61
62/// The desktops and the managers bound to them.
63pub struct WorkspaceState {
64    managers: Vec<Manager>,
65    count: u32,
66    current: u32,
67    /// The outputs every group spans, by display id, kept so a removed output is still left
68    /// after its node is gone.
69    outputs: Vec<(u32, Output)>,
70    global: Option<GlobalId>,
71}
72
73impl WorkspaceState {
74    /// Publish the global, with one desktop until the policy says otherwise.
75    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
87/// The desktops a command describes, bounded: at least one, and the current among them.
88fn bounded(count: u32, current: u32) -> (u32, u32) {
89    let count = count.max(1);
90    (count, current.min(count - 1))
91}
92
93/// A desktop's coordinates: one dimension, its index, as the array of words the protocol
94/// carries in the byte order the client reads them back in.
95fn coordinates(index: u32) -> Vec<u8> {
96    index.to_ne_bytes().to_vec()
97}
98
99/// A desktop's state bits: active for the current one, none for the rest.
100fn state_bits(index: u32, current: u32) -> State {
101    if index == current {
102        State::Active
103    } else {
104        State::empty()
105    }
106}
107
108/// The desktops whose active bit moves when the current one changes.
109fn 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
116/// The outputs a group spans: every screen of its own, never a view onto another's.
117fn 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
126/// Send a pager a handle for desktop `index`, assigned to its group, or `None` where its
127/// connection is gone.
128fn 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
156/// The policy said how many desktops there are and which is current: every pager is told
157/// what changed, in one batch.
158pub 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
193/// The layout's outputs changed: every group enters the new ones and leaves the gone ones.
194pub 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
227/// A client bound a `wl_output` of an output its group already spans: the group enters it,
228/// since an enter reaches only the objects the client held when it was sent.
229pub 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    /// A desktop's coordinates are its index alone, one word in the client's byte order.
362    #[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    /// The current desktop alone carries the active bit, and a switch moves it from one
370    /// desktop to the other and touches no third.
371    #[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    /// A command naming no desktop, or a current one past the count, is bounded to what the
384    /// protocol can show.
385    #[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}