cosmic_client_toolkit/
toplevel_info.rs

1use std::{
2    collections::{HashMap, HashSet},
3    sync::OnceLock,
4};
5
6use cosmic_protocols::toplevel_info::v1::client::{
7    zcosmic_toplevel_handle_v1, zcosmic_toplevel_info_v1,
8};
9use sctk::registry::RegistryState;
10use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, Weak, protocol::wl_output};
11use wayland_protocols::ext::{
12    foreign_toplevel_list::v1::client::{
13        ext_foreign_toplevel_handle_v1, ext_foreign_toplevel_list_v1,
14    },
15    workspace::v1::client::ext_workspace_handle_v1,
16};
17
18use crate::GlobalData;
19
20#[derive(Clone, Debug, Default)]
21pub struct ToplevelGeometry {
22    pub x: i32,
23    pub y: i32,
24    pub width: i32,
25    pub height: i32,
26}
27
28#[derive(Clone, Debug)]
29pub struct ToplevelInfo {
30    pub title: String,
31    pub app_id: String,
32    pub identifier: String,
33    /// Requires zcosmic_toplevel_info_v1 version 2
34    pub state: HashSet<zcosmic_toplevel_handle_v1::State>,
35    /// Requires zcosmic_toplevel_info_v1 version 2
36    pub output: HashSet<wl_output::WlOutput>,
37    /// Requires zcosmic_toplevel_info_v1 version 2
38    pub geometry: HashMap<wl_output::WlOutput, ToplevelGeometry>,
39    /// Requires zcosmic_toplevel_info_v1 version 3
40    pub workspace: HashSet<ext_workspace_handle_v1::ExtWorkspaceHandleV1>,
41    /// Requires zcosmic_toplevel_info_v1 version 2
42    pub cosmic_toplevel: Option<zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1>,
43    pub foreign_toplevel: ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
44}
45
46#[derive(Debug)]
47struct ToplevelData {
48    current_info: Option<ToplevelInfo>,
49    pending_info: ToplevelInfo,
50    has_cosmic_info: bool,
51}
52
53impl ToplevelData {
54    fn new(foreign_toplevel: ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1) -> Self {
55        let pending_info = ToplevelInfo {
56            title: String::new(),
57            app_id: String::new(),
58            identifier: String::new(),
59            state: HashSet::new(),
60            output: HashSet::new(),
61            geometry: HashMap::new(),
62            workspace: HashSet::new(),
63            cosmic_toplevel: None,
64            foreign_toplevel,
65        };
66        Self {
67            current_info: None,
68            pending_info,
69            has_cosmic_info: false,
70        }
71    }
72
73    fn cosmic_toplevel(&self) -> Option<&zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1> {
74        self.pending_info.cosmic_toplevel.as_ref()
75    }
76
77    fn foreign_toplevel(&self) -> &ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1 {
78        &self.pending_info.foreign_toplevel
79    }
80}
81
82#[doc(hidden)]
83#[derive(Default)]
84pub struct ToplevelUserData {
85    cosmic_toplevel: OnceLock<Option<Weak<zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1>>>,
86}
87
88/// Handler for `ext-foreign-toplevel-list-v1`, and optionally
89/// `cosmic-toplevel-info-unstable-v1` which extends it with additional information.
90#[derive(Debug)]
91pub struct ToplevelInfoState {
92    pub foreign_toplevel_list: ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1,
93    pub cosmic_toplevel_info: Option<zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1>,
94    toplevels: Vec<ToplevelData>,
95}
96
97impl ToplevelInfoState {
98    pub fn try_new<D>(registry: &RegistryState, qh: &QueueHandle<D>) -> Option<Self>
99    where
100        D: Dispatch<zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, GlobalData>
101            + Dispatch<ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, GlobalData>
102            + 'static,
103    {
104        let foreign_toplevel_list = registry
105            .bind_one::<ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, _, _>(
106                qh,
107                1..=1,
108                GlobalData,
109            )
110            .ok()?;
111        let cosmic_toplevel_info = registry
112            .bind_one::<zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, _, _>(
113                qh,
114                2..=3,
115                GlobalData,
116            )
117            .ok();
118
119        Some(Self {
120            foreign_toplevel_list,
121            cosmic_toplevel_info,
122            toplevels: Vec::new(),
123        })
124    }
125
126    pub fn new<D>(registry: &RegistryState, qh: &QueueHandle<D>) -> Self
127    where
128        D: Dispatch<zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, GlobalData>
129            + Dispatch<ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, GlobalData>
130            + 'static,
131    {
132        Self::try_new(registry, qh).unwrap()
133    }
134
135    pub fn info(
136        &self,
137        toplevel: &ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
138    ) -> Option<&ToplevelInfo> {
139        self.toplevels
140            .iter()
141            .find(|data| data.foreign_toplevel() == toplevel)?
142            .current_info
143            .as_ref()
144    }
145
146    pub fn toplevels(&self) -> impl Iterator<Item = &ToplevelInfo> {
147        self.toplevels
148            .iter()
149            .filter_map(|data| data.current_info.as_ref())
150    }
151}
152
153pub trait ToplevelInfoHandler: Sized {
154    fn toplevel_info_state(&mut self) -> &mut ToplevelInfoState;
155
156    fn new_toplevel(
157        &mut self,
158        conn: &Connection,
159        qh: &QueueHandle<Self>,
160        toplevel: &ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
161    );
162
163    fn update_toplevel(
164        &mut self,
165        conn: &Connection,
166        qh: &QueueHandle<Self>,
167        toplevel: &ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
168    );
169
170    fn toplevel_closed(
171        &mut self,
172        conn: &Connection,
173        qh: &QueueHandle<Self>,
174        toplevel: &ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
175    );
176
177    fn info_done(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>) {}
178
179    fn finished(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>) {}
180}
181
182impl<D> Dispatch<zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, GlobalData, D>
183    for ToplevelInfoState
184where
185    D: Dispatch<zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, GlobalData>
186        + Dispatch<zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, GlobalData>
187        + ToplevelInfoHandler
188        + 'static,
189{
190    fn event(
191        state: &mut D,
192        _proxy: &zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1,
193        event: zcosmic_toplevel_info_v1::Event,
194        _: &GlobalData,
195        conn: &Connection,
196        qh: &QueueHandle<D>,
197    ) {
198        match event {
199            zcosmic_toplevel_info_v1::Event::Done => {
200                state.info_done(conn, qh);
201            }
202            // Not used in protocol version 2
203            zcosmic_toplevel_info_v1::Event::Toplevel { .. }
204            | zcosmic_toplevel_info_v1::Event::Finished => {}
205            _ => unreachable!(),
206        }
207    }
208
209    wayland_client::event_created_child!(D, zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1, [
210        zcosmic_toplevel_info_v1::EVT_TOPLEVEL_OPCODE => (zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, GlobalData)
211    ]);
212}
213
214impl<D> Dispatch<zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, GlobalData, D>
215    for ToplevelInfoState
216where
217    D: Dispatch<zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, GlobalData>
218        + ToplevelInfoHandler
219        + 'static,
220{
221    fn event(
222        state: &mut D,
223        toplevel: &zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1,
224        event: zcosmic_toplevel_handle_v1::Event,
225        _: &GlobalData,
226        _conn: &Connection,
227        _qh: &QueueHandle<D>,
228    ) {
229        let data = &mut state
230            .toplevel_info_state()
231            .toplevels
232            .iter_mut()
233            .find(|data| data.cosmic_toplevel() == Some(toplevel))
234            .expect("Received event for dead toplevel");
235        match event {
236            zcosmic_toplevel_handle_v1::Event::OutputEnter { output } => {
237                data.pending_info.output.insert(output);
238            }
239            zcosmic_toplevel_handle_v1::Event::OutputLeave { output } => {
240                data.pending_info.output.remove(&output);
241                data.pending_info.geometry.remove(&output);
242            }
243            // Ignore legacy workspace handle events
244            zcosmic_toplevel_handle_v1::Event::WorkspaceEnter { .. }
245            | zcosmic_toplevel_handle_v1::Event::WorkspaceLeave { .. } => {}
246            zcosmic_toplevel_handle_v1::Event::ExtWorkspaceEnter { workspace } => {
247                data.pending_info.workspace.insert(workspace);
248            }
249            zcosmic_toplevel_handle_v1::Event::ExtWorkspaceLeave { workspace } => {
250                data.pending_info.workspace.remove(&workspace);
251            }
252            zcosmic_toplevel_handle_v1::Event::State { state } => {
253                data.has_cosmic_info = true;
254                data.pending_info.state.clear();
255                for value in state.chunks_exact(4) {
256                    if let Ok(state) = zcosmic_toplevel_handle_v1::State::try_from(
257                        u32::from_ne_bytes(value[0..4].try_into().unwrap()),
258                    ) {
259                        data.pending_info.state.insert(state);
260                    }
261                }
262            }
263            zcosmic_toplevel_handle_v1::Event::Geometry {
264                output,
265                x,
266                y,
267                width,
268                height,
269            } => {
270                data.pending_info.geometry.insert(
271                    output,
272                    ToplevelGeometry {
273                        x,
274                        y,
275                        width,
276                        height,
277                    },
278                );
279            }
280            // Not used in protocol version 2
281            zcosmic_toplevel_handle_v1::Event::AppId { .. }
282            | zcosmic_toplevel_handle_v1::Event::Title { .. }
283            | zcosmic_toplevel_handle_v1::Event::Done { .. }
284            | zcosmic_toplevel_handle_v1::Event::Closed { .. } => {}
285            _ => unreachable!(),
286        }
287    }
288}
289
290impl<D> Dispatch<ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, GlobalData, D>
291    for ToplevelInfoState
292where
293    D: Dispatch<ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, GlobalData>
294        + Dispatch<ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, ToplevelUserData>
295        + Dispatch<zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1, GlobalData>
296        + ToplevelInfoHandler
297        + 'static,
298{
299    fn event(
300        state: &mut D,
301        proxy: &ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1,
302        event: ext_foreign_toplevel_list_v1::Event,
303        _: &GlobalData,
304        conn: &Connection,
305        qh: &QueueHandle<D>,
306    ) {
307        match event {
308            ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } => {
309                let info_state = state.toplevel_info_state();
310                let mut toplevel_data = ToplevelData::new(toplevel.clone());
311                let cosmic_toplevel =
312                    info_state
313                        .cosmic_toplevel_info
314                        .as_ref()
315                        .map(|cosmic_toplevel_info| {
316                            cosmic_toplevel_info.get_cosmic_toplevel(&toplevel, qh, GlobalData)
317                        });
318                toplevel
319                    .data::<ToplevelUserData>()
320                    .unwrap()
321                    .cosmic_toplevel
322                    .set(cosmic_toplevel.as_ref().map(|t| t.downgrade()))
323                    .unwrap();
324                toplevel_data.pending_info.cosmic_toplevel = cosmic_toplevel;
325                info_state.toplevels.push(toplevel_data);
326            }
327            ext_foreign_toplevel_list_v1::Event::Finished => {
328                state.finished(conn, qh);
329                proxy.destroy();
330            }
331            _ => unreachable!(),
332        }
333    }
334
335    wayland_client::event_created_child!(D, ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1, [
336        ext_foreign_toplevel_list_v1::EVT_TOPLEVEL_OPCODE => (ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, Default::default())
337    ]);
338}
339
340impl<D> Dispatch<ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, ToplevelUserData, D>
341    for ToplevelInfoState
342where
343    D: Dispatch<ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, ToplevelUserData>
344        + ToplevelInfoHandler,
345{
346    fn event(
347        state: &mut D,
348        handle: &ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
349        event: ext_foreign_toplevel_handle_v1::Event,
350        _data: &ToplevelUserData,
351        conn: &Connection,
352        qh: &QueueHandle<D>,
353    ) {
354        let data = &mut state
355            .toplevel_info_state()
356            .toplevels
357            .iter_mut()
358            .find(|data| data.foreign_toplevel() == handle)
359            .expect("Received event for dead toplevel");
360        match event {
361            ext_foreign_toplevel_handle_v1::Event::Closed => {
362                state.toplevel_closed(conn, qh, handle);
363
364                let toplevels = &mut state.toplevel_info_state().toplevels;
365                if let Some(idx) = toplevels
366                    .iter()
367                    .position(|data| data.foreign_toplevel() == handle)
368                {
369                    toplevels.remove(idx);
370                }
371            }
372            ext_foreign_toplevel_handle_v1::Event::Done => {
373                if data.cosmic_toplevel().is_some() && !data.has_cosmic_info {
374                    // Don't call `new_toplevel` if we have the `ext_foreign_toplevel_handle_v1`,
375                    // but don't have any `zcosmic_toplevel_handle_v1` events yet.
376                    return;
377                }
378
379                let is_new = data.current_info.is_none();
380                data.current_info = Some(data.pending_info.clone());
381                if is_new {
382                    state.new_toplevel(conn, qh, handle);
383                } else {
384                    state.update_toplevel(conn, qh, handle);
385                }
386            }
387            ext_foreign_toplevel_handle_v1::Event::Title { title } => {
388                data.pending_info.title = title;
389            }
390            ext_foreign_toplevel_handle_v1::Event::AppId { app_id } => {
391                data.pending_info.app_id = app_id;
392            }
393            ext_foreign_toplevel_handle_v1::Event::Identifier { identifier } => {
394                data.pending_info.identifier = identifier;
395            }
396            _ => unreachable!(),
397        }
398    }
399}
400
401#[macro_export]
402macro_rules! delegate_toplevel_info {
403    ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => {
404        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
405            $crate::cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_info_v1::ZcosmicToplevelInfoV1: $crate::GlobalData
406        ] => $crate::toplevel_info::ToplevelInfoState);
407        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
408            $crate::cosmic_protocols::toplevel_info::v1::client::zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1: $crate::GlobalData
409        ] => $crate::toplevel_info::ToplevelInfoState);
410        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
411            $crate::wayland_protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_list_v1::ExtForeignToplevelListV1: $crate::GlobalData
412        ] => $crate::toplevel_info::ToplevelInfoState);
413        $crate::wayland_client::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [
414            $crate::wayland_protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1: $crate::toplevel_info::ToplevelUserData
415        ] => $crate::toplevel_info::ToplevelInfoState);
416    };
417}