cosmic_client_toolkit/
workspace.rs

1use cosmic_protocols::workspace::v2::client::{
2    zcosmic_workspace_handle_v2, zcosmic_workspace_manager_v2,
3};
4use sctk::registry::{GlobalProxy, RegistryState};
5use std::collections::HashSet;
6use wayland_client::{Connection, Dispatch, QueueHandle, WEnum, protocol::wl_output};
7use wayland_protocols::ext::workspace::v1::client::{
8    ext_workspace_group_handle_v1, ext_workspace_handle_v1, ext_workspace_manager_v1,
9};
10
11use crate::GlobalData;
12
13#[derive(Clone, Debug)]
14pub struct WorkspaceGroup {
15    pub handle: ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
16    pub capabilities: ext_workspace_group_handle_v1::GroupCapabilities,
17    pub outputs: Vec<wl_output::WlOutput>,
18    pub workspaces: HashSet<ext_workspace_handle_v1::ExtWorkspaceHandleV1>,
19}
20
21#[derive(Debug)]
22struct WorkspaceGroupData {
23    handle: ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
24    current: Option<WorkspaceGroup>,
25    pending: Option<WorkspaceGroup>,
26}
27
28impl WorkspaceGroupData {
29    fn pending(&mut self) -> &mut WorkspaceGroup {
30        if self.pending.is_none() {
31            self.pending = Some(self.current.clone().unwrap_or(WorkspaceGroup {
32                handle: self.handle.clone(),
33                capabilities: ext_workspace_group_handle_v1::GroupCapabilities::empty(),
34                outputs: Vec::new(),
35                workspaces: HashSet::new(),
36            }));
37        }
38        self.pending.as_mut().unwrap()
39    }
40
41    fn commit_pending(&mut self) {
42        if let Some(pending) = self.pending.take() {
43            self.current = Some(pending);
44        }
45    }
46}
47
48#[derive(Clone, Debug)]
49pub struct Workspace {
50    pub handle: ext_workspace_handle_v1::ExtWorkspaceHandleV1,
51    pub cosmic_handle: Option<zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2>,
52    pub name: String,
53    pub coordinates: Vec<u32>,
54    pub state: ext_workspace_handle_v1::State,
55    pub cosmic_state: zcosmic_workspace_handle_v2::State,
56    pub capabilities: ext_workspace_handle_v1::WorkspaceCapabilities,
57    pub cosmic_capabilities: zcosmic_workspace_handle_v2::WorkspaceCapabilities,
58    pub tiling: Option<WEnum<zcosmic_workspace_handle_v2::TilingState>>,
59    pub id: Option<String>,
60}
61
62#[derive(Debug)]
63struct WorkspaceData {
64    handle: ext_workspace_handle_v1::ExtWorkspaceHandleV1,
65    cosmic_handle: Option<zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2>,
66    current: Option<Workspace>,
67    pending: Option<Workspace>,
68    has_cosmic_info: bool,
69}
70
71impl WorkspaceData {
72    fn pending(&mut self) -> &mut Workspace {
73        if self.pending.is_none() {
74            self.pending = Some(self.current.clone().unwrap_or(Workspace {
75                handle: self.handle.clone(),
76                cosmic_handle: self.cosmic_handle.clone(),
77                name: String::new(),
78                coordinates: Vec::new(),
79                state: ext_workspace_handle_v1::State::empty(),
80                cosmic_state: zcosmic_workspace_handle_v2::State::empty(),
81                capabilities: ext_workspace_handle_v1::WorkspaceCapabilities::empty(),
82                cosmic_capabilities: zcosmic_workspace_handle_v2::WorkspaceCapabilities::empty(),
83                tiling: None,
84                id: None,
85            }));
86        }
87        self.pending.as_mut().unwrap()
88    }
89
90    fn commit_pending(&mut self) {
91        if let Some(pending) = self.pending.take() {
92            self.current = Some(pending);
93        }
94    }
95}
96
97#[derive(Debug)]
98pub struct WorkspaceState {
99    workspace_groups: Vec<WorkspaceGroupData>,
100    workspaces: Vec<WorkspaceData>,
101    manager: GlobalProxy<ext_workspace_manager_v1::ExtWorkspaceManagerV1>,
102    cosmic_manager: GlobalProxy<zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2>,
103}
104
105impl WorkspaceState {
106    pub fn new<D>(registry: &RegistryState, qh: &QueueHandle<D>) -> Self
107    where
108        D: Dispatch<ext_workspace_manager_v1::ExtWorkspaceManagerV1, GlobalData>
109            + Dispatch<zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2, GlobalData>
110            + 'static,
111    {
112        Self {
113            workspace_groups: Vec::new(),
114            workspaces: Vec::new(),
115            manager: GlobalProxy::from(registry.bind_one(qh, 1..=1, GlobalData)),
116            cosmic_manager: GlobalProxy::from(registry.bind_one(qh, 1..=2, GlobalData)),
117        }
118    }
119
120    pub fn workspace_manager(
121        &self,
122    ) -> &GlobalProxy<ext_workspace_manager_v1::ExtWorkspaceManagerV1> {
123        &self.manager
124    }
125
126    pub fn workspace_groups(&self) -> impl Iterator<Item = &WorkspaceGroup> {
127        self.workspace_groups
128            .iter()
129            .filter_map(|data| data.current.as_ref())
130    }
131
132    pub fn workspace_group_info(
133        &self,
134        handle: &ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
135    ) -> Option<&WorkspaceGroup> {
136        self.workspace_groups
137            .iter()
138            .find(|g| g.handle == *handle)?
139            .current
140            .as_ref()
141    }
142
143    pub fn workspaces(&self) -> impl Iterator<Item = &Workspace> {
144        self.workspaces
145            .iter()
146            .filter_map(|data| data.current.as_ref())
147    }
148
149    pub fn workspace_info(
150        &self,
151        handle: &ext_workspace_handle_v1::ExtWorkspaceHandleV1,
152    ) -> Option<&Workspace> {
153        self.workspaces
154            .iter()
155            .find(|g| g.handle == *handle)?
156            .current
157            .as_ref()
158    }
159}
160
161pub trait WorkspaceHandler {
162    fn workspace_state(&mut self) -> &mut WorkspaceState;
163
164    // TODO: Added/remove/update methods? How to do that with groups and workspaces?
165    fn done(&mut self);
166}
167
168impl<D> Dispatch<ext_workspace_manager_v1::ExtWorkspaceManagerV1, GlobalData, D> for WorkspaceState
169where
170    D: Dispatch<ext_workspace_manager_v1::ExtWorkspaceManagerV1, GlobalData>
171        + Dispatch<ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, GlobalData>
172        + Dispatch<ext_workspace_handle_v1::ExtWorkspaceHandleV1, GlobalData>
173        + Dispatch<zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2, GlobalData>
174        + WorkspaceHandler
175        + 'static,
176{
177    fn event(
178        state: &mut D,
179        _: &ext_workspace_manager_v1::ExtWorkspaceManagerV1,
180        event: ext_workspace_manager_v1::Event,
181        _: &GlobalData,
182        _: &Connection,
183        qh: &QueueHandle<D>,
184    ) {
185        match event {
186            ext_workspace_manager_v1::Event::WorkspaceGroup { workspace_group } => {
187                state
188                    .workspace_state()
189                    .workspace_groups
190                    .push(WorkspaceGroupData {
191                        handle: workspace_group,
192                        current: None,
193                        pending: None,
194                    });
195            }
196            ext_workspace_manager_v1::Event::Workspace { workspace } => {
197                let cosmic_handle =
198                    state
199                        .workspace_state()
200                        .cosmic_manager
201                        .get()
202                        .ok()
203                        .map(|cosmic_manager| {
204                            cosmic_manager.get_cosmic_workspace(&workspace, qh, GlobalData)
205                        });
206                state.workspace_state().workspaces.push(WorkspaceData {
207                    handle: workspace,
208                    cosmic_handle,
209                    current: None,
210                    pending: None,
211                    has_cosmic_info: false,
212                });
213            }
214            ext_workspace_manager_v1::Event::Done => {
215                // If any workspace doesn't have cosmic info yet, we should wait for the
216                // server to send, it instead of providing incomplete data.
217                // Ignore this `done`, and wait for the one sent after the cosmic info.
218                if state.workspace_state().cosmic_manager.get().is_ok()
219                    && state
220                        .workspace_state()
221                        .workspaces
222                        .iter()
223                        .any(|w| !w.has_cosmic_info)
224                {
225                    return;
226                }
227                for data in &mut state.workspace_state().workspace_groups {
228                    data.commit_pending();
229                }
230                for data in &mut state.workspace_state().workspaces {
231                    data.commit_pending();
232                }
233                state.done();
234            }
235            ext_workspace_manager_v1::Event::Finished => {}
236            _ => unreachable!(),
237        }
238    }
239
240    wayland_client::event_created_child!(D, ext_workspace_manager_v1::ExtWorkspaceManagerV1, [
241        ext_workspace_manager_v1::EVT_WORKSPACE_GROUP_OPCODE => (ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, GlobalData),
242        ext_workspace_manager_v1::EVT_WORKSPACE_OPCODE => (ext_workspace_handle_v1::ExtWorkspaceHandleV1, GlobalData)
243    ]);
244}
245
246impl<D> Dispatch<ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, GlobalData, D>
247    for WorkspaceState
248where
249    D: Dispatch<ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, GlobalData>
250        + Dispatch<ext_workspace_handle_v1::ExtWorkspaceHandleV1, GlobalData>
251        + WorkspaceHandler
252        + 'static,
253{
254    fn event(
255        state: &mut D,
256        handle: &ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1,
257        event: ext_workspace_group_handle_v1::Event,
258        _: &GlobalData,
259        _: &Connection,
260        _: &QueueHandle<D>,
261    ) {
262        let group = &mut state
263            .workspace_state()
264            .workspace_groups
265            .iter_mut()
266            .find(|group| &group.handle == handle)
267            .unwrap();
268        match event {
269            ext_workspace_group_handle_v1::Event::Capabilities { capabilities } => {
270                group.pending().capabilities = bitflags_retained(capabilities);
271            }
272            ext_workspace_group_handle_v1::Event::OutputEnter { output } => {
273                group.pending().outputs.push(output);
274            }
275            ext_workspace_group_handle_v1::Event::OutputLeave { output } => {
276                let pending = group.pending();
277                if let Some(idx) = pending.outputs.iter().position(|x| x == &output) {
278                    pending.outputs.remove(idx);
279                }
280            }
281            ext_workspace_group_handle_v1::Event::WorkspaceEnter { workspace } => {
282                group.pending().workspaces.insert(workspace);
283            }
284            ext_workspace_group_handle_v1::Event::WorkspaceLeave { workspace } => {
285                group.pending().workspaces.remove(&workspace);
286            }
287            ext_workspace_group_handle_v1::Event::Removed => {
288                if let Some(idx) = state
289                    .workspace_state()
290                    .workspace_groups
291                    .iter()
292                    .position(|group| &group.handle == handle)
293                {
294                    state.workspace_state().workspace_groups.remove(idx);
295                }
296            }
297            _ => unreachable!(),
298        }
299    }
300}
301
302impl<D> Dispatch<ext_workspace_handle_v1::ExtWorkspaceHandleV1, GlobalData, D> for WorkspaceState
303where
304    D: Dispatch<ext_workspace_handle_v1::ExtWorkspaceHandleV1, GlobalData> + WorkspaceHandler,
305{
306    fn event(
307        state: &mut D,
308        handle: &ext_workspace_handle_v1::ExtWorkspaceHandleV1,
309        event: ext_workspace_handle_v1::Event,
310        _: &GlobalData,
311        _: &Connection,
312        _: &QueueHandle<D>,
313    ) {
314        let workspace = state
315            .workspace_state()
316            .workspaces
317            .iter_mut()
318            .find(|w| &w.handle == handle)
319            .unwrap();
320        match event {
321            ext_workspace_handle_v1::Event::Name { name } => {
322                workspace.pending().name = name;
323            }
324            ext_workspace_handle_v1::Event::Coordinates { coordinates } => {
325                workspace.pending().coordinates = coordinates
326                    .chunks(4)
327                    .map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap()))
328                    .collect();
329            }
330            ext_workspace_handle_v1::Event::State { state } => {
331                workspace.pending().state = bitflags_retained(state);
332            }
333            ext_workspace_handle_v1::Event::Capabilities { capabilities } => {
334                workspace.pending().capabilities = bitflags_retained(capabilities);
335            }
336            ext_workspace_handle_v1::Event::Id { id } => {
337                workspace.pending().id = Some(id);
338            }
339            ext_workspace_handle_v1::Event::Removed => {
340                // Protocol guarantees it will already have been removed from group,
341                // so no need to do that here.
342
343                if let Some(idx) = state
344                    .workspace_state()
345                    .workspaces
346                    .iter()
347                    .position(|w| &w.handle == handle)
348                {
349                    state.workspace_state().workspaces.remove(idx);
350                }
351            }
352            _ => unreachable!(),
353        }
354    }
355}
356
357impl<D> Dispatch<zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2, GlobalData, D>
358    for WorkspaceState
359where
360    D: Dispatch<zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2, GlobalData>
361        + WorkspaceHandler
362        + 'static,
363{
364    fn event(
365        _: &mut D,
366        _: &zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2,
367        _: zcosmic_workspace_manager_v2::Event,
368        _: &GlobalData,
369        _: &Connection,
370        _: &QueueHandle<D>,
371    ) {
372        unreachable!()
373    }
374}
375
376impl<D> Dispatch<zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2, GlobalData, D>
377    for WorkspaceState
378where
379    D: Dispatch<zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2, GlobalData>
380        + WorkspaceHandler
381        + 'static,
382{
383    fn event(
384        state: &mut D,
385        handle: &zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2,
386        event: zcosmic_workspace_handle_v2::Event,
387        _: &GlobalData,
388        _: &Connection,
389        _: &QueueHandle<D>,
390    ) {
391        let workspace = state
392            .workspace_state()
393            .workspaces
394            .iter_mut()
395            .find(|w| w.cosmic_handle.as_ref() == Some(&handle))
396            .unwrap();
397        match event {
398            zcosmic_workspace_handle_v2::Event::Capabilities { capabilities } => {
399                workspace.pending().cosmic_capabilities = bitflags_retained(capabilities);
400                workspace.has_cosmic_info = true;
401            }
402            zcosmic_workspace_handle_v2::Event::TilingState { state } => {
403                workspace.pending().tiling = Some(state);
404            }
405            zcosmic_workspace_handle_v2::Event::State { state } => {
406                workspace.pending().cosmic_state = bitflags_retained(state);
407            }
408            _ => unreachable!(),
409        }
410    }
411}
412
413// Convert bitflags `WEnum` to bitflag type, retaining unrecognized bits
414fn bitflags_retained<T: bitflags::Flags<Bits = u32>>(flags: WEnum<T>) -> T {
415    match flags {
416        WEnum::Value(value) => value,
417        WEnum::Unknown(value) => T::from_bits_retain(value),
418    }
419}
420
421#[macro_export]
422macro_rules! delegate_workspace {
423    ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
424        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
425            $crate::wayland_protocols::ext::workspace::v1::client::ext_workspace_manager_v1::ExtWorkspaceManagerV1: $crate::GlobalData
426        ] => $crate::workspace::WorkspaceState);
427        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
428            $crate::wayland_protocols::ext::workspace::v1::client::ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1: $crate::GlobalData
429        ] => $crate::workspace::WorkspaceState);
430        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
431            $crate::wayland_protocols::ext::workspace::v1::client::ext_workspace_handle_v1::ExtWorkspaceHandleV1: $crate::GlobalData
432        ] => $crate::workspace::WorkspaceState);
433
434        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
435            $crate::cosmic_protocols::workspace::v2::client::zcosmic_workspace_manager_v2::ZcosmicWorkspaceManagerV2: $crate::GlobalData
436        ] => $crate::workspace::WorkspaceState);
437        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
438            $crate::cosmic_protocols::workspace::v2::client::zcosmic_workspace_handle_v2::ZcosmicWorkspaceHandleV2: $crate::GlobalData
439        ] => $crate::workspace::WorkspaceState);
440    };
441}