Skip to main content

smithay_client_toolkit/shell/xdg/
popup.rs

1use crate::{
2    compositor::{Surface, SurfaceData},
3    dispatch2::Dispatch2,
4    error::GlobalError,
5    globals::ProvidesBoundGlobal,
6    shell::{xdg::XdgShellSurface, WaylandSurface},
7};
8use std::sync::{
9    atomic::{AtomicI32, AtomicU32, Ordering::Relaxed},
10    Arc, Weak,
11};
12use wayland_client::{
13    protocol::{wl_compositor::WlCompositor, wl_surface},
14    Connection, Dispatch, Proxy, QueueHandle,
15};
16use wayland_protocols::xdg::shell::client::{xdg_popup, xdg_positioner, xdg_surface, xdg_wm_base};
17
18#[derive(Debug, Clone)]
19pub struct Popup {
20    inner: Arc<PopupInner>,
21}
22
23impl Eq for Popup {}
24impl PartialEq for Popup {
25    fn eq(&self, other: &Popup) -> bool {
26        Arc::ptr_eq(&self.inner, &other.inner)
27    }
28}
29
30#[derive(Debug)]
31pub struct PopupData {
32    inner: Weak<PopupInner>,
33}
34
35#[derive(Debug)]
36struct PopupInner {
37    surface: XdgShellSurface,
38    xdg_popup: xdg_popup::XdgPopup,
39    pending_position: (AtomicI32, AtomicI32),
40    pending_dimensions: (AtomicI32, AtomicI32),
41    pending_token: AtomicU32,
42    configure_state: AtomicU32,
43}
44
45impl Popup {
46    /// Create a new popup.
47    ///
48    /// This creates the popup and sends the initial commit.  You must wait for
49    /// [`PopupHandler::configure`] to commit contents to the surface.
50    pub fn new<D>(
51        parent: &xdg_surface::XdgSurface,
52        position: &xdg_positioner::XdgPositioner,
53        qh: &QueueHandle<D>,
54        compositor: &impl ProvidesBoundGlobal<WlCompositor, 6>,
55        wm_base: &impl ProvidesBoundGlobal<xdg_wm_base::XdgWmBase, 5>,
56    ) -> Result<Popup, GlobalError>
57    where
58        D: Dispatch<wl_surface::WlSurface, SurfaceData<()>>
59            + Dispatch<xdg_surface::XdgSurface, PopupData>
60            + Dispatch<xdg_popup::XdgPopup, PopupData>
61            + PopupHandler
62            + 'static,
63    {
64        let surface = Surface::new(compositor, qh)?;
65        let popup = Self::from_surface(Some(parent), position, qh, surface, wm_base)?;
66        popup.wl_surface().commit();
67        Ok(popup)
68    }
69
70    /// Create a new popup from an existing surface.
71    ///
72    /// If you do not specify a parent surface, you must configure the parent using an alternate
73    /// function such as [`LayerSurface::get_popup`] prior to committing the surface, or you will
74    /// get an `invalid_popup_parent` protocol error.
75    ///
76    /// [`LayerSurface::get_popup`]: wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1::ZwlrLayerSurfaceV1::get_popup
77    pub fn from_surface<D>(
78        parent: Option<&xdg_surface::XdgSurface>,
79        position: &xdg_positioner::XdgPositioner,
80        qh: &QueueHandle<D>,
81        surface: impl Into<Surface>,
82        wm_base: &impl ProvidesBoundGlobal<xdg_wm_base::XdgWmBase, 5>,
83    ) -> Result<Popup, GlobalError>
84    where
85        D: Dispatch<xdg_surface::XdgSurface, PopupData>
86            + Dispatch<xdg_popup::XdgPopup, PopupData>
87            + 'static,
88    {
89        let surface = surface.into();
90        let wm_base = wm_base.bound_global()?;
91        // Freeze the queue during the creation of the Arc to avoid a race between events on the
92        // new objects being processed and the Weak in the PopupData becoming usable.
93        let freeze = qh.freeze();
94        let inner = Arc::new_cyclic(|weak| {
95            let xdg_surface = wm_base.get_xdg_surface(
96                surface.wl_surface(),
97                qh,
98                PopupData { inner: weak.clone() },
99            );
100            let surface = XdgShellSurface { surface, xdg_surface };
101            let xdg_popup = surface.xdg_surface().get_popup(
102                parent,
103                position,
104                qh,
105                PopupData { inner: weak.clone() },
106            );
107
108            PopupInner {
109                surface,
110                xdg_popup,
111                pending_position: (AtomicI32::new(0), AtomicI32::new(0)),
112                pending_dimensions: (AtomicI32::new(-1), AtomicI32::new(-1)),
113                pending_token: AtomicU32::new(0),
114                configure_state: AtomicU32::new(PopupConfigure::STATE_NEW),
115            }
116        });
117        drop(freeze);
118        Ok(Popup { inner })
119    }
120
121    pub fn xdg_popup(&self) -> &xdg_popup::XdgPopup {
122        &self.inner.xdg_popup
123    }
124
125    pub fn xdg_shell_surface(&self) -> &XdgShellSurface {
126        &self.inner.surface
127    }
128
129    pub fn xdg_surface(&self) -> &xdg_surface::XdgSurface {
130        self.inner.surface.xdg_surface()
131    }
132
133    pub fn wl_surface(&self) -> &wl_surface::WlSurface {
134        self.inner.surface.wl_surface()
135    }
136
137    /// Reposition the popup
138    /// Important: This is only available in wayland version 3 and after
139    pub fn reposition(&self, position: &xdg_positioner::XdgPositioner, token: u32) {
140        if self.xdg_popup().version() >= 3 {
141            self.xdg_popup().reposition(position, token);
142        }
143    }
144}
145
146impl WaylandSurface for Popup {
147    fn wl_surface(&self) -> &wl_surface::WlSurface {
148        self.wl_surface()
149    }
150}
151
152impl PopupData {
153    /// Get a new handle to the Popup
154    ///
155    /// This returns `None` if the popup has been destroyed.
156    pub fn popup(&self) -> Option<Popup> {
157        let inner = self.inner.upgrade()?;
158        Some(Popup { inner })
159    }
160}
161
162impl Drop for PopupInner {
163    fn drop(&mut self) {
164        self.xdg_popup.destroy();
165    }
166}
167
168#[derive(Debug, Clone)]
169#[non_exhaustive]
170pub struct PopupConfigure {
171    /// (x,y) relative to parent surface window geometry
172    pub position: (i32, i32),
173    pub width: i32,
174    pub height: i32,
175    pub serial: u32,
176    pub kind: ConfigureKind,
177}
178
179#[derive(Debug, Clone)]
180#[non_exhaustive]
181pub enum ConfigureKind {
182    /// Initial configure for this popup
183    Initial,
184    /// The configure is due to an xdg_positioner with set_reactive requested
185    Reactive,
186    /// The configure is due to a reposition request with this token
187    Reposition { token: u32 },
188}
189
190impl PopupConfigure {
191    const STATE_NEW: u32 = 0;
192    const STATE_CONFIGURED: u32 = 1;
193    const STATE_REPOSITION_ACK: u32 = 2;
194}
195
196pub trait PopupHandler: Sized {
197    /// The popup has been configured.
198    fn configure(
199        &mut self,
200        conn: &Connection,
201        qh: &QueueHandle<Self>,
202        popup: &Popup,
203        config: PopupConfigure,
204    );
205
206    /// The popup was dismissed by the compositor and should be destroyed.
207    fn done(&mut self, conn: &Connection, qh: &QueueHandle<Self>, popup: &Popup);
208}
209
210impl<D> Dispatch2<xdg_surface::XdgSurface, D> for PopupData
211where
212    D: PopupHandler,
213{
214    fn event(
215        &self,
216        data: &mut D,
217        xdg_surface: &xdg_surface::XdgSurface,
218        event: xdg_surface::Event,
219        conn: &Connection,
220        qh: &QueueHandle<D>,
221    ) {
222        let popup = match self.popup() {
223            Some(popup) => popup,
224            None => return,
225        };
226        let inner = &popup.inner;
227        match event {
228            xdg_surface::Event::Configure { serial } => {
229                xdg_surface.ack_configure(serial);
230                let x = inner.pending_position.0.load(Relaxed);
231                let y = inner.pending_position.1.load(Relaxed);
232                let width = inner.pending_dimensions.0.load(Relaxed);
233                let height = inner.pending_dimensions.1.load(Relaxed);
234                let kind =
235                    match inner.configure_state.swap(PopupConfigure::STATE_CONFIGURED, Relaxed) {
236                        PopupConfigure::STATE_NEW => ConfigureKind::Initial,
237                        PopupConfigure::STATE_CONFIGURED => ConfigureKind::Reactive,
238                        PopupConfigure::STATE_REPOSITION_ACK => {
239                            ConfigureKind::Reposition { token: inner.pending_token.load(Relaxed) }
240                        }
241                        _ => unreachable!(),
242                    };
243
244                let config = PopupConfigure { position: (x, y), width, height, serial, kind };
245
246                data.configure(conn, qh, &popup, config);
247            }
248            _ => unreachable!(),
249        }
250    }
251}
252
253impl<D> Dispatch2<xdg_popup::XdgPopup, D> for PopupData
254where
255    D: PopupHandler,
256{
257    fn event(
258        &self,
259        data: &mut D,
260        _: &xdg_popup::XdgPopup,
261        event: xdg_popup::Event,
262        conn: &Connection,
263        qh: &QueueHandle<D>,
264    ) {
265        let popup = match self.popup() {
266            Some(popup) => popup,
267            None => return,
268        };
269        let inner = &popup.inner;
270        match event {
271            xdg_popup::Event::Configure { x, y, width, height } => {
272                inner.pending_position.0.store(x, Relaxed);
273                inner.pending_position.1.store(y, Relaxed);
274                inner.pending_dimensions.0.store(width, Relaxed);
275                inner.pending_dimensions.1.store(height, Relaxed);
276            }
277            xdg_popup::Event::PopupDone => {
278                data.done(conn, qh, &popup);
279            }
280            xdg_popup::Event::Repositioned { token } => {
281                inner.pending_token.store(token, Relaxed);
282                inner.configure_state.store(PopupConfigure::STATE_REPOSITION_ACK, Relaxed);
283            }
284            _ => unreachable!(),
285        }
286    }
287}