ashpd/window_identifier/
mod.rs

1use std::{fmt, str::FromStr};
2
3#[cfg(all(
4    any(feature = "gtk4_wayland", feature = "gtk4_x11"),
5    feature = "backend"
6))]
7use ::gtk4 as gtk;
8#[cfg(all(feature = "raw_handle", feature = "gtk4"))]
9use raw_window_handle::{
10    DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle,
11};
12#[cfg(feature = "raw_handle")]
13use raw_window_handle::{RawDisplayHandle, RawWindowHandle};
14use serde::{ser::Serializer, Deserialize, Serialize};
15use zbus::zvariant::Type;
16/// Most portals interact with the user by showing dialogs.
17///
18/// These dialogs should generally be placed on top of the application window
19/// that triggered them. To arrange this, the compositor needs to know about the
20/// application window. Many portal requests expect a [`WindowIdentifier`] for
21/// this reason.
22///
23/// Under X11, the [`WindowIdentifier`] should have the form `x11:XID`, where
24/// XID is the XID of the application window in hexadecimal. Under Wayland, it
25/// should have the form `wayland:HANDLE`, where HANDLE is a surface handle
26/// obtained with the [xdg-foreign](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/unstable/xdg-foreign/xdg-foreign-unstable-v2.xml) protocol.
27///
28/// See also [Parent window identifiers](https://flatpak.github.io/xdg-desktop-portal/docs/window-identifiers.html).
29///
30/// # Usage
31///
32/// ## From an X11 XID
33///
34/// ```rust,ignore
35/// let identifier = WindowIdentifier::from_xid(212321);
36///
37/// /// Open some portals
38/// ```
39///
40/// ## From a Wayland Surface
41///
42/// The `wayland` feature must be enabled. The exported surface handle will be
43/// unexported on `Drop`.
44///
45/// ```text
46/// // let wl_surface = some_surface;
47/// // let identifier = WindowIdentifier::from_wayland(wl_surface).await;
48///
49/// /// Open some portals
50/// ```
51///
52/// Or using a raw `wl_surface` pointer
53///
54/// ```text
55/// // let wl_surface_ptr = some_surface;
56/// // let wl_display_ptr = corresponding_display;
57/// // let identifier = WindowIdentifier::from_wayland_raw(wl_surface_ptr, wl_display_ptr).await;
58///
59/// /// Open some portals
60/// ```
61///
62/// ## With GTK 4
63///
64/// The feature `gtk4` must be enabled. You can get a
65/// [`WindowIdentifier`] from a [`IsA<gtk4::Native>`](https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/struct.Native.html) using `WindowIdentifier::from_native`
66///
67/// ```rust, ignore
68/// let widget = gtk4::Button::new();
69///
70/// let ctx = glib::MainContext::default();
71/// ctx.spawn_async(async move {
72///     let identifier = WindowIdentifier::from_native(&widget.native().unwrap()).await;
73///
74///     /// Open some portals
75/// });
76/// ```
77/// The constructor should return a valid identifier under both X11 and Wayland
78/// and fallback to the [`Default`] implementation otherwise.
79///
80/// ## Other Toolkits
81///
82/// If you have access to `RawWindowHandle` you can convert it to a
83/// [`WindowIdentifier`] with
84///
85/// ```rust, ignore
86/// let handle = RawWindowHandle::Xlib(XlibHandle::empty());
87/// let identifier = WindowIdentifier::from_raw_handle(handle, None);
88///
89/// /// Open some portals
90/// ```
91#[derive(Type)]
92#[zvariant(signature = "s")]
93#[doc(alias = "XdpParent")]
94#[non_exhaustive]
95pub enum WindowIdentifier {
96    /// Gtk 4 Window Identifier
97    #[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))]
98    #[doc(hidden)]
99    Gtk4(Gtk4WindowIdentifier),
100    #[cfg(feature = "wayland")]
101    #[doc(hidden)]
102    Wayland(WaylandWindowIdentifier),
103    #[doc(hidden)]
104    X11(WindowIdentifierType),
105}
106
107unsafe impl Send for WindowIdentifier {}
108unsafe impl Sync for WindowIdentifier {}
109
110impl Serialize for WindowIdentifier {
111    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
112    where
113        S: Serializer,
114    {
115        serializer.serialize_str(&self.to_string())
116    }
117}
118
119impl std::fmt::Display for WindowIdentifier {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            #[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))]
123            Self::Gtk4(identifier) => f.write_str(&format!("{identifier}")),
124            #[cfg(feature = "wayland")]
125            Self::Wayland(identifier) => f.write_str(&format!("{identifier}")),
126            Self::X11(identifier) => f.write_str(&format!("{identifier}")),
127        }
128    }
129}
130
131impl std::fmt::Debug for WindowIdentifier {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.debug_tuple("WindowIdentifier")
134            .field(&format!("{self}"))
135            .finish()
136    }
137}
138
139impl WindowIdentifier {
140    #[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))]
141    #[cfg_attr(docsrs, doc(cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))))]
142    /// Creates a [`WindowIdentifier`] from a [`gtk4::Native`](https://docs.gtk.org/gtk4/class.Native.html).
143    ///
144    /// The constructor returns a valid handle under both Wayland & x11.
145    ///
146    /// **Note** the function has to be async as the Wayland handle retrieval
147    /// API is async as well.
148    #[doc(alias = "xdp_parent_new_gtk")]
149    pub async fn from_native(native: &impl ::gtk4::prelude::IsA<::gtk4::Native>) -> Option<Self> {
150        Gtk4WindowIdentifier::new(native).await.map(Self::Gtk4)
151    }
152
153    #[cfg(feature = "raw_handle")]
154    #[cfg_attr(docsrs, doc(cfg(feature = "raw_handle")))]
155    /// Create an instance of [`WindowIdentifier`] from a
156    /// [`RawWindowHandle`](raw_window_handle::RawWindowHandle).
157    ///
158    /// The constructor returns a valid handle under both Wayland & X11.
159    ///
160    /// This method is only async and requires a `RawDisplayHandle` only for
161    /// Wayland handles.
162    pub async fn from_raw_handle(
163        window_handle: &RawWindowHandle,
164        display_handle: Option<&RawDisplayHandle>,
165    ) -> Option<Self> {
166        use raw_window_handle::RawWindowHandle::{Xcb, Xlib};
167        #[cfg(feature = "wayland")]
168        use raw_window_handle::{
169            RawDisplayHandle::Wayland as DisplayHandle, RawWindowHandle::Wayland,
170        };
171        match (window_handle, display_handle) {
172            #[cfg(feature = "wayland")]
173            (Wayland(wl_handle), Some(DisplayHandle(wl_display))) => unsafe {
174                Self::from_wayland_raw(wl_handle.surface.as_ptr(), wl_display.display.as_ptr())
175                    .await
176            },
177            (Xlib(x_handle), _) => Some(Self::from_xid(x_handle.window)),
178            (Xcb(x_handle), _) => Some(Self::from_xid(x_handle.window.get().into())),
179            _ => None,
180        }
181    }
182
183    /// Create an instance of [`WindowIdentifier`] from an X11 window's XID.
184    pub fn from_xid(xid: std::os::raw::c_ulong) -> Self {
185        Self::X11(WindowIdentifierType::X11(xid))
186    }
187
188    #[cfg(feature = "wayland")]
189    #[cfg_attr(docsrs, doc(cfg(feature = "wayland")))]
190    /// Create an instance of [`WindowIdentifier`] from a Wayland surface.
191    ///
192    /// # Safety
193    ///
194    /// Both surface and display pointers have to be valid . You must
195    /// ensure the `display_ptr` lives longer than the returned
196    /// `WindowIdentifier`.
197    pub async unsafe fn from_wayland_raw(
198        surface_ptr: *mut std::ffi::c_void,
199        display_ptr: *mut std::ffi::c_void,
200    ) -> Option<Self> {
201        WaylandWindowIdentifier::from_raw(surface_ptr, display_ptr)
202            .await
203            .map(Self::Wayland)
204    }
205
206    #[cfg(feature = "wayland")]
207    #[cfg_attr(docsrs, doc(cfg(feature = "wayland")))]
208    /// Create an instance of [`WindowIdentifier`] from a Wayland surface.
209    pub async fn from_wayland(
210        surface: &wayland_client::protocol::wl_surface::WlSurface,
211    ) -> Option<Self> {
212        WaylandWindowIdentifier::new(surface)
213            .await
214            .map(Self::Wayland)
215    }
216}
217
218#[cfg(all(feature = "raw_handle", feature = "gtk4"))]
219impl HasDisplayHandle for WindowIdentifier {
220    /// Convert a [`WindowIdentifier`] to
221    /// [`RawDisplayHandle`](raw_window_handle::RawDisplayHandle`).
222    ///
223    /// # Panics
224    ///
225    /// If you attempt to convert a [`WindowIdentifier`] created from a
226    /// [`RawDisplayHandle`](raw_window_handle::RawDisplayHandle`) instead of
227    /// the gtk4 constructors.
228    fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
229        match self {
230            #[cfg(feature = "gtk4")]
231            Self::Gtk4(identifier) => Ok(identifier.as_raw_display_handle()),
232            _ => unreachable!(),
233        }
234    }
235}
236
237#[cfg(all(feature = "raw_handle", feature = "gtk4"))]
238impl HasWindowHandle for WindowIdentifier {
239    /// Convert a [`WindowIdentifier`] to
240    /// [`RawWindowHandle`](raw_window_handle::RawWindowHandle`).
241    ///
242    /// # Panics
243    ///
244    /// If you attempt to convert a [`WindowIdentifier`] created from a
245    /// [`RawWindowHandle`](raw_window_handle::RawWindowHandle`) instead of
246    /// the gtk4 constructors.
247    fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
248        match self {
249            #[cfg(feature = "gtk4")]
250            Self::Gtk4(identifier) => Ok(identifier.as_raw_window_handle()),
251            _ => unreachable!(),
252        }
253    }
254}
255
256/// Supported WindowIdentifier kinds
257#[derive(Debug, Clone, PartialEq, Eq, Type)]
258#[zvariant(signature = "s")]
259pub enum WindowIdentifierType {
260    /// X11.
261    X11(std::os::raw::c_ulong),
262    #[allow(dead_code)]
263    /// Wayland.
264    Wayland(String),
265}
266
267impl fmt::Display for WindowIdentifierType {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        match self {
270            Self::X11(xid) => f.write_str(&format!("x11:{xid:x}")),
271            Self::Wayland(handle) => f.write_str(&format!("wayland:{handle}")),
272        }
273    }
274}
275
276impl FromStr for WindowIdentifierType {
277    type Err = PortalError;
278    fn from_str(s: &str) -> Result<Self, Self::Err> {
279        let (kind, handle) = s
280            .split_once(':')
281            .ok_or_else(|| PortalError::InvalidArgument("Invalid Window Identifier".to_owned()))?;
282        match kind {
283            "x11" => {
284                let handle = handle.trim_start_matches("0x");
285                Ok(Self::X11(
286                    std::os::raw::c_ulong::from_str_radix(handle, 16)
287                        .map_err(|_| PortalError::InvalidArgument(format!("Wrong XID {handle}")))?,
288                ))
289            }
290            "wayland" => Ok(Self::Wayland(handle.to_owned())),
291            t => Err(PortalError::InvalidArgument(format!(
292                "Invalid Window Identifier type {t}",
293            ))),
294        }
295    }
296}
297
298impl<'de> Deserialize<'de> for WindowIdentifierType {
299    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
300    where
301        D: serde::Deserializer<'de>,
302    {
303        let handle = String::deserialize(deserializer)?;
304        Self::from_str(&handle)
305            .map_err(|e| serde::de::Error::custom(format!("Invalid Window identifier {e}")))
306    }
307}
308
309impl Serialize for WindowIdentifierType {
310    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
311    where
312        S: Serializer,
313    {
314        self.to_string().serialize(serializer)
315    }
316}
317
318impl WindowIdentifierType {
319    /// Sets the given window as a modal child of the window represented by
320    /// self.
321    ///
322    /// The different combinations of window types and their support is as per
323    /// follows.
324    /// - Wayland child window, Wayland parent window - supported.
325    /// - Wayland child window, X11 parent window - unsupported.
326    /// - X11 child window, Wayland parent window - unsupported.
327    /// - X11 child window, X11 parent window - supported.
328    ///
329    /// This is useful in backend implementations as the portal dialogs have to
330    /// be modal and grouped together with the application that launched the
331    /// request.
332    ///
333    /// Realizes the window if it is not realized yet.
334    ///
335    /// Returns `true` on success.
336    #[cfg(all(
337        any(feature = "gtk4_wayland", feature = "gtk4_x11"),
338        feature = "backend"
339    ))]
340    #[cfg_attr(
341        docsrs,
342        doc(cfg(all(
343            any(feature = "gtk4_wayland", feature = "gtk4_x11"),
344            feature = "backend"
345        )))
346    )]
347    pub fn set_parent_of(&self, window: &impl gtk::prelude::IsA<gtk::Window>) -> bool {
348        use gtk::prelude::*;
349
350        let window = window.as_ref();
351
352        let surface = match window.surface() {
353            Some(surface) => surface,
354            None => {
355                WidgetExt::realize(window);
356                window.surface().unwrap()
357            }
358        };
359
360        window.set_modal(true);
361
362        match self {
363            #[cfg(feature = "gtk4_x11")]
364            WindowIdentifierType::X11(xid) => {
365                use gdk4x11::{x11::xlib, X11Display, X11Surface};
366
367                let display = match WidgetExt::display(window).dynamic_cast::<X11Display>() {
368                    Ok(display) => display,
369                    Err(_) => {
370                        #[cfg(feature = "tracing")]
371                        tracing::warn!("Failed to get X11 display");
372                        return false;
373                    }
374                };
375                let surface = match surface.dynamic_cast::<X11Surface>() {
376                    Ok(surface) => surface,
377                    Err(_) => {
378                        #[cfg(feature = "tracing")]
379                        tracing::warn!("Failed to get X11 surface");
380                        return false;
381                    }
382                };
383                unsafe {
384                    // Based on GNOME's libgxdp -
385                    // https://gitlab.gnome.org/GNOME/libgxdp/-/blob/e6c11f2812cad0a43e847ec97bfc1c67bf50be52/src/gxdp-external-window-x11.c#L90-105
386                    let xdisplay = display.xdisplay();
387                    xlib::XSetTransientForHint(xdisplay, surface.xid(), *xid);
388                    let net_wm_window_type_atom =
389                        gdk4x11::x11_get_xatom_by_name_for_display(&display, "_NET_WM_WINDOW_TYPE");
390                    let net_wm_window_type_dialog_atom = gdk4x11::x11_get_xatom_by_name_for_display(
391                        &display,
392                        "_NET_WM_WINDOW_DIALOG_TYPE",
393                    );
394                    let data: *const u8 = &(net_wm_window_type_dialog_atom as u8);
395                    xlib::XChangeProperty(
396                        xdisplay,
397                        surface.xid(),
398                        net_wm_window_type_atom,
399                        xlib::XA_ATOM,
400                        32,
401                        xlib::PropModeReplace,
402                        data,
403                        1,
404                    );
405                    true
406                }
407            }
408            #[cfg(feature = "gtk4_wayland")]
409            WindowIdentifierType::Wayland(handle) => {
410                use gdk4wayland::WaylandToplevel;
411
412                let toplevel = match surface.dynamic_cast::<WaylandToplevel>() {
413                    Ok(toplevel) => toplevel,
414                    Err(_) => {
415                        #[cfg(feature = "tracing")]
416                        tracing::warn!("Failed to get toplevel from surface");
417                        return false;
418                    }
419                };
420                toplevel.set_transient_for_exported(handle)
421            }
422            #[cfg(not(all(feature = "gtk4_x11", feature = "gtk4_wayland")))]
423            _ => false,
424        }
425    }
426}
427
428#[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))]
429mod gtk4;
430
431#[cfg(any(feature = "gtk4_wayland", feature = "gtk4_x11"))]
432pub use self::gtk4::Gtk4WindowIdentifier;
433use crate::PortalError;
434
435#[cfg(feature = "wayland")]
436mod wayland;
437
438#[cfg(feature = "wayland")]
439pub use self::wayland::WaylandWindowIdentifier;
440
441#[cfg(test)]
442mod tests {
443    use std::str::FromStr;
444
445    use super::WindowIdentifier;
446    use crate::window_identifier::WindowIdentifierType;
447
448    #[test]
449    fn test_serialize() {
450        let x11 = WindowIdentifier::from_xid(1024);
451        assert_eq!(x11.to_string(), "x11:400");
452
453        assert_eq!(
454            WindowIdentifierType::from_str("x11:11432").unwrap(),
455            WindowIdentifierType::X11(70706)
456        );
457
458        // A valid x11 window identifier shouldn't be prefixed with 0x, this is kept for
459        // backwards compatibility and compatibility with backends which
460        // implicitly strip the prefix with e.g. `strtol`
461        assert_eq!(
462            WindowIdentifierType::from_str("x11:0x502a").unwrap(),
463            WindowIdentifierType::X11(20522)
464        );
465
466        assert_eq!(
467            WindowIdentifierType::from_str("wayland:Somerandomchars").unwrap(),
468            WindowIdentifierType::Wayland("Somerandomchars".to_owned())
469        );
470        assert!(WindowIdentifierType::from_str("some_handle").is_err());
471        assert!(WindowIdentifierType::from_str("some_type:some_handle").is_err());
472    }
473}