Skip to main content

vst3_host/
embed.rs

1//! Embed a plugin editor inside a host window (e.g. an egui/eframe window), as an
2//! alternative to the standalone [`PluginWindow`](crate::PluginWindow).
3//!
4//! Instead of opening its own top-level OS window, the plugin's native editor view is
5//! parented as a child of a window your UI framework already owns, positioned to track a
6//! region you allocate. You provide the parent window's [`RawWindowHandle`] and a target
7//! rectangle (in logical points, top-left origin — the egui convention) each frame.
8//!
9//! Implemented on macOS (verified), Windows, and Linux/X11. A Wayland `RawWindowHandle` is
10//! rejected explicitly: VST 3.8 requires the host to provide a compositor connection through
11//! `IWaylandHost`, not merely pass the application's system-compositor `wl_surface`. Other
12//! platforms return an error from [`EmbeddedEditor::embed`]. Requires the `egui-widgets` feature.
13//!
14//! Sizing goes both ways. The host proposes a size through [`EmbeddedEditor::set_rect`] and the
15//! plugin may adjust or refuse it; the plugin proposes one through
16//! [`EmbeddedEditor::take_resize_request`], which the host should poll each frame and answer by
17//! allocating that much space.
18//!
19//! On Windows the parent window and its message loop remain owned by the UI framework, so this
20//! type cannot intercept `WM_DPICHANGED`. Forward framework scale-factor changes explicitly with
21//! [`Plugin::set_editor_scale_factor`](crate::Plugin::set_editor_scale_factor); the initial child
22//! DPI is communicated automatically when embedding.
23#![cfg(feature = "egui-widgets")]
24
25use crate::error::{Error, Result};
26use crate::plugin::Plugin;
27use raw_window_handle::RawWindowHandle;
28use std::cell::Cell;
29use std::sync::{Arc, Mutex};
30
31// The native child view/window the plugin's editor is parented into, per platform. All three
32// expose the same `new` / `set_rect` pair, so the cross-platform code below names only this.
33#[cfg(target_os = "linux")]
34use linux::LinuxEmbed as PlatformEmbed;
35#[cfg(target_os = "macos")]
36use macos::MacEmbed as PlatformEmbed;
37#[cfg(target_os = "windows")]
38use windows::WinEmbed as PlatformEmbed;
39
40/// A rectangle in the host view's logical points, **top-left origin** (egui convention).
41#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct EditorRect {
43    /// Left edge, points from the window's left.
44    pub x: f32,
45    /// Top edge, points from the window's top.
46    pub y: f32,
47    /// Width in points.
48    pub width: f32,
49    /// Height in points.
50    pub height: f32,
51}
52
53/// The most recent size negotiation with the plugin: what the host asked for, and what the
54/// plugin's `checkSizeConstraint` answered (or the fallback used when it refused outright).
55#[derive(Clone, Copy)]
56struct NegotiatedSize {
57    requested: (i32, i32),
58    accepted: (i32, i32),
59}
60
61/// A plugin editor embedded into a host window. Drop it (or call [`Self::close`]) to detach
62/// the editor and remove the child view.
63///
64/// Not `Sync`: it caches the last negotiated size in a [`Cell`], and every method belongs on
65/// the UI thread that owns the parent window anyway.
66pub struct EmbeddedEditor {
67    plugin: Arc<Mutex<Plugin>>,
68    /// Cached so a host that calls [`Self::set_rect`] every frame does not send the plugin an
69    /// `onSize` for a size it already answered.
70    negotiated: Cell<Option<NegotiatedSize>>,
71    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
72    inner: PlatformEmbed,
73}
74
75impl EmbeddedEditor {
76    /// Embed `plugin`'s editor as a child of `parent`, at `rect`.
77    ///
78    /// Must be called on the UI/main thread (where your event loop runs). `parent` is the
79    /// host window's handle (e.g. from `eframe::Frame::window_handle()`).
80    ///
81    /// On Windows, continue forwarding later DPI/scale changes through
82    /// [`Plugin::set_editor_scale_factor`](crate::Plugin::set_editor_scale_factor). An embedded
83    /// child does not own the parent framework's window procedure.
84    ///
85    /// Sizing the editor to `rect` is best-effort: a fixed-size editor, or one behind process
86    /// isolation (where resize requests are not marshalled), keeps its own size and the embed
87    /// still succeeds. Read the size the child actually got back from
88    /// [`Self::try_set_rect`].
89    pub fn embed(
90        plugin: Arc<Mutex<Plugin>>,
91        parent: RawWindowHandle,
92        rect: EditorRect,
93    ) -> Result<Self> {
94        #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
95        {
96            let inner = PlatformEmbed::new(&plugin, parent, rect)?;
97            Ok(Self::sized(plugin, inner, rect))
98        }
99        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
100        {
101            let _ = (&plugin, parent, rect);
102            Err(Error::Other(
103                "editor embedding is not implemented on this platform".to_string(),
104            ))
105        }
106    }
107
108    /// Assemble the editor and apply the caller's initial rectangle, tolerating a plugin that
109    /// declines the size — the view is attached either way, so failing here would throw away a
110    /// working editor over a cosmetic mismatch.
111    #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
112    fn sized(plugin: Arc<Mutex<Plugin>>, inner: PlatformEmbed, rect: EditorRect) -> Self {
113        let editor = Self {
114            plugin,
115            negotiated: Cell::new(None),
116            inner,
117        };
118        if let Err(error) = editor.try_set_rect(rect) {
119            log::warn!(
120                "embedded editor kept its own size, the requested one was declined: {error} \
121                 (call EmbeddedEditor::try_set_rect to read the size in effect)"
122            );
123        }
124        editor
125    }
126
127    /// Reposition/resize the embedded editor to track `rect`. Call each frame so the editor
128    /// follows the host layout (scroll, window resize).
129    ///
130    /// The position always follows `rect`. The *size* is whatever the plugin accepts: a
131    /// fixed-size editor keeps its own dimensions, so the child view may be smaller or larger
132    /// than the rectangle you asked for. Use [`Self::try_set_rect`] to learn which.
133    ///
134    /// Implemented on macOS, Windows and Linux/X11 — the same platforms
135    /// [`Self::embed`] supports. A no-op on any other platform, where `embed` cannot have
136    /// succeeded in the first place.
137    pub fn set_rect(&self, rect: EditorRect) {
138        let _ = self.try_set_rect(rect);
139    }
140
141    /// Fallible variant of [`Self::set_rect`] that reports a rejected resize.
142    ///
143    /// On success the returned rectangle carries the dimensions the plugin accepted after
144    /// applying its `checkSizeConstraint` rules, which may be smaller or larger than the ones
145    /// requested. On failure the child view has still been moved to `rect`'s position and kept
146    /// at the last size the plugin accepted — a plugin that refuses to resize does not stop the
147    /// editor from tracking the host layout.
148    ///
149    /// Repeating the same size is cheap: the plugin is only asked when the requested dimensions
150    /// differ from the last ones it answered.
151    pub fn try_set_rect(&self, rect: EditorRect) -> Result<EditorRect> {
152        let requested = validated_size(rect)?;
153
154        // Already negotiated: move the child, skip the COM round-trip. Host layouts call this
155        // every frame and an `onSize` per frame is spam the plugin has to redraw for.
156        if let Some(previous) = self.negotiated.get() {
157            if previous.requested == requested {
158                return Ok(self.place(rect, previous.accepted));
159            }
160        }
161
162        // VST 3 order: the container the view lives in takes its new size first, then the view
163        // is told about it (`IPlugView::onSize`, via `Plugin::resize_editor`).
164        self.place(rect, requested);
165
166        let outcome = self.negotiate(requested);
167        let accepted = match &outcome {
168            Ok(accepted) => *accepted,
169            // Refusal is not fatal. Fall back to the size the plugin last accepted, else the
170            // size it reports for itself, else leave the container where we just put it.
171            Err(_) => self.fallback_size().unwrap_or(requested),
172        };
173        self.negotiated.set(Some(NegotiatedSize {
174            requested,
175            accepted,
176        }));
177
178        let placed = if accepted == requested {
179            EditorRect {
180                width: requested.0 as f32,
181                height: requested.1 as f32,
182                ..rect
183            }
184        } else {
185            self.place(rect, accepted)
186        };
187        outcome.map(|_| placed)
188    }
189
190    /// Poll for a resize the *plugin* asked for through VST 3's `IPlugFrame::resizeView`, in
191    /// pixels, consuming it.
192    ///
193    /// An embedded editor cannot resize the host's layout on its own. Call this each frame
194    /// while the editor is open; when it answers, allocate that much space in your UI and feed
195    /// the resulting rectangle back through [`Self::set_rect`]. Ignoring it leaves the plugin's
196    /// view drawing at a size its container does not match (clipped or letterboxed).
197    ///
198    /// Returns `None` when nothing is pending, when the plugin mutex is momentarily held
199    /// elsewhere (the request stays queued for the next poll), and always for a
200    /// process-isolated plugin, whose editor is not bridged across the boundary.
201    pub fn take_resize_request(&self) -> Option<(i32, i32)> {
202        try_lock(&self.plugin)?.take_editor_resize_request()
203    }
204
205    /// Ask the plugin to accept `size`, reporting what it settled on.
206    fn negotiate(&self, (width, height): (i32, i32)) -> Result<(i32, i32)> {
207        self.plugin
208            .lock()
209            .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
210            .resize_editor(width, height)
211    }
212
213    /// The size to keep when the plugin refuses a resize.
214    fn fallback_size(&self) -> Option<(i32, i32)> {
215        if let Some(previous) = self.negotiated.get() {
216            return Some(previous.accepted);
217        }
218        try_lock(&self.plugin)?.get_editor_size().ok()
219    }
220
221    /// Move the native child to `rect`'s position at `size`, and report the rectangle applied.
222    fn place(&self, rect: EditorRect, size: (i32, i32)) -> EditorRect {
223        let placed = EditorRect {
224            width: size.0 as f32,
225            height: size.1 as f32,
226            ..rect
227        };
228        // The plugin lock is deliberately not held here. On Windows, resizing the HWND may
229        // synchronously dispatch window messages back into host code.
230        #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
231        self.inner.set_rect(placed);
232        placed
233    }
234
235    /// Detach the editor and remove the child view (also done on drop).
236    pub fn close(self) {}
237}
238
239/// Reject rectangles a native window system cannot represent, and round the size to pixels.
240fn validated_size(rect: EditorRect) -> Result<(i32, i32)> {
241    let finite = rect.x.is_finite()
242        && rect.y.is_finite()
243        && rect.width.is_finite()
244        && rect.height.is_finite();
245    if !finite
246        || rect.width <= 0.0
247        || rect.height <= 0.0
248        || rect.width > i32::MAX as f32
249        || rect.height > i32::MAX as f32
250    {
251        return Err(Error::Other(
252            "embedded editor rectangle must be finite with positive dimensions".to_string(),
253        ));
254    }
255    Ok((rect.width.round() as i32, rect.height.round() as i32))
256}
257
258/// Take the plugin lock without blocking, recovering a lock poisoned by an unrelated panic.
259///
260/// Used by the per-frame polls: the audio callback holds this mutex for each block, and
261/// stalling the UI thread behind it would stutter the host. Missing a frame costs nothing —
262/// both callers retry.
263fn try_lock(plugin: &Mutex<Plugin>) -> Option<std::sync::MutexGuard<'_, Plugin>> {
264    match plugin.try_lock() {
265        Ok(guard) => Some(guard),
266        Err(std::sync::TryLockError::Poisoned(poison)) => Some(poison.into_inner()),
267        Err(std::sync::TryLockError::WouldBlock) => None,
268    }
269}
270
271#[cfg(test)]
272mod rect_tests {
273    use super::*;
274
275    fn rect(width: f32, height: f32) -> EditorRect {
276        EditorRect {
277            x: 4.0,
278            y: 8.0,
279            width,
280            height,
281        }
282    }
283
284    #[test]
285    fn rounds_the_requested_size_to_whole_pixels() {
286        assert_eq!(validated_size(rect(799.4, 600.5)).unwrap(), (799, 601));
287    }
288
289    #[test]
290    fn rejects_sizes_a_window_system_cannot_represent() {
291        for bad in [
292            rect(0.0, 600.0),
293            rect(800.0, -1.0),
294            rect(f32::NAN, 600.0),
295            rect(800.0, f32::INFINITY),
296        ] {
297            assert!(
298                validated_size(bad).is_err(),
299                "{bad:?} should not reach the plugin"
300            );
301        }
302    }
303
304    #[test]
305    fn rejects_a_non_finite_position_even_with_a_valid_size() {
306        let mut bad = rect(800.0, 600.0);
307        bad.x = f32::NAN;
308        assert!(validated_size(bad).is_err());
309    }
310}
311
312impl Drop for EmbeddedEditor {
313    fn drop(&mut self) {
314        // Detach the plugin's view first; the platform child view is torn down by `inner`.
315        if let Ok(mut p) = self.plugin.lock() {
316            let _ = p.close_editor();
317        }
318    }
319}
320
321#[cfg(target_os = "macos")]
322mod macos {
323    use super::*;
324    use objc2::{rc::Retained, MainThreadMarker, MainThreadOnly};
325    use objc2_app_kit::NSView;
326    use objc2_foundation::{NSPoint, NSRect, NSSize};
327
328    pub struct MacEmbed {
329        parent: Retained<NSView>,
330        child: Retained<NSView>,
331    }
332
333    impl MacEmbed {
334        pub fn new(
335            plugin: &Arc<Mutex<Plugin>>,
336            parent: RawWindowHandle,
337            rect: EditorRect,
338        ) -> Result<Self> {
339            let mtm = MainThreadMarker::new().ok_or_else(|| {
340                Error::Other("editor embedding must run on the main thread".to_string())
341            })?;
342            let RawWindowHandle::AppKit(h) = parent else {
343                return Err(Error::Other(
344                    "expected an AppKit window handle for the parent".to_string(),
345                ));
346            };
347            // The host owns `ns_view`; retain it so it outlives our use.
348            let parent: Retained<NSView> =
349                unsafe { Retained::retain(h.ns_view.as_ptr() as *mut NSView) }
350                    .ok_or_else(|| Error::Other("null parent NSView".to_string()))?;
351
352            // Create the container child view the plugin attaches into.
353            let frame = NSRect::new(
354                NSPoint::new(rect.x as f64, 0.0),
355                NSSize::new(rect.width as f64, rect.height as f64),
356            );
357            let child = NSView::initWithFrame(NSView::alloc(mtm), frame);
358            parent.addSubview(&child);
359
360            // SAFETY: `child` is a live NSView, retained by this `MacEmbed` for as long as the
361            // editor is attached — `Drop` removes it from its superview only after
362            // `EmbeddedEditor::drop` has closed the editor.
363            let handle = unsafe {
364                crate::plugin::WindowHandle::from_nsview(
365                    Retained::as_ptr(&child) as *mut std::ffi::c_void
366                )
367            };
368            plugin
369                .lock()
370                .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
371                .open_editor(handle)?;
372
373            Ok(Self { parent, child })
374        }
375
376        pub fn set_rect(&self, rect: EditorRect) {
377            // Convert egui's top-left origin to the parent view's coordinate space. AppKit
378            // views are bottom-left origin unless flipped, so flip Y against the parent's
379            // current height (which changes as the window resizes).
380            let flipped = self.parent.isFlipped();
381            let parent_height = self.parent.bounds().size.height;
382            let y = if flipped {
383                rect.y as f64
384            } else {
385                parent_height - (rect.y + rect.height) as f64
386            };
387            let frame = NSRect::new(
388                NSPoint::new(rect.x as f64, y),
389                NSSize::new(rect.width as f64, rect.height as f64),
390            );
391            self.child.setFrame(frame);
392        }
393    }
394
395    impl Drop for MacEmbed {
396        fn drop(&mut self) {
397            self.child.removeFromSuperview();
398        }
399    }
400}
401
402#[cfg(target_os = "windows")]
403mod windows {
404    use super::*;
405    use winapi::shared::windef::HWND;
406    use winapi::um::libloaderapi::GetModuleHandleW;
407    use winapi::um::winuser::{
408        CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassExW, SetWindowPos, ShowWindow,
409        CS_HREDRAW, CS_VREDRAW, SWP_NOZORDER, SW_SHOW, WNDCLASSEXW, WS_CHILD, WS_VISIBLE,
410    };
411
412    /// A plugin editor embedded as a child `HWND` of the host window.
413    pub struct WinEmbed {
414        child: HWND,
415    }
416
417    impl WinEmbed {
418        pub fn new(
419            plugin: &Arc<Mutex<Plugin>>,
420            parent: RawWindowHandle,
421            rect: EditorRect,
422        ) -> Result<Self> {
423            let RawWindowHandle::Win32(h) = parent else {
424                return Err(Error::Other(
425                    "expected a Win32 window handle for the parent".to_string(),
426                ));
427            };
428            unsafe {
429                let parent_hwnd = h.hwnd.get() as HWND;
430                let hinstance = GetModuleHandleW(std::ptr::null());
431
432                // Register a child window class (idempotent across calls).
433                let class_name: Vec<u16> = "VST3EmbeddedEditor\0".encode_utf16().collect();
434                let mut wc: WNDCLASSEXW = std::mem::zeroed();
435                wc.cbSize = std::mem::size_of::<WNDCLASSEXW>() as u32;
436                wc.style = CS_HREDRAW | CS_VREDRAW;
437                wc.lpfnWndProc = Some(DefWindowProcW);
438                wc.hInstance = hinstance;
439                wc.lpszClassName = class_name.as_ptr();
440                RegisterClassExW(&wc);
441
442                let child = CreateWindowExW(
443                    0,
444                    class_name.as_ptr(),
445                    std::ptr::null(),
446                    WS_CHILD | WS_VISIBLE,
447                    rect.x as i32,
448                    rect.y as i32,
449                    rect.width as i32,
450                    rect.height as i32,
451                    parent_hwnd,
452                    std::ptr::null_mut(),
453                    hinstance,
454                    std::ptr::null_mut(),
455                );
456                if child.is_null() {
457                    return Err(Error::Other("Failed to create child window".to_string()));
458                }
459
460                // SAFETY: `child` was just created above and null-checked; it is destroyed only
461                // after the editor is detached (the error arm below, or `Drop`).
462                let handle = crate::plugin::WindowHandle::from_hwnd(child as *mut std::ffi::c_void);
463                let mut plugin = plugin
464                    .lock()
465                    .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?;
466                let dpi = winapi::um::winuser::GetDpiForWindow(child);
467                if dpi > 0 {
468                    if let Err(error) = plugin.set_editor_scale_factor(dpi as f32 / 96.0) {
469                        drop(plugin);
470                        DestroyWindow(child);
471                        return Err(error);
472                    }
473                }
474                if let Err(e) = plugin.open_editor(handle) {
475                    drop(plugin);
476                    DestroyWindow(child);
477                    return Err(e);
478                }
479                drop(plugin);
480                ShowWindow(child, SW_SHOW);
481                Ok(Self { child })
482            }
483        }
484
485        pub fn set_rect(&self, rect: EditorRect) {
486            unsafe {
487                SetWindowPos(
488                    self.child,
489                    std::ptr::null_mut(),
490                    rect.x as i32,
491                    rect.y as i32,
492                    rect.width as i32,
493                    rect.height as i32,
494                    SWP_NOZORDER,
495                );
496            }
497        }
498    }
499
500    impl Drop for WinEmbed {
501        fn drop(&mut self) {
502            unsafe {
503                DestroyWindow(self.child);
504            }
505        }
506    }
507}
508
509#[cfg(target_os = "linux")]
510mod linux {
511    use super::*;
512    use xcb::{x, Xid, XidNew};
513
514    const WAYLAND_UNSUPPORTED: &str = "Wayland VST3 editor embedding requires a host compositor \
515        plus IWaylandHost/IWaylandFrame; a RawWindowHandle supplies only the system-compositor \
516        wl_surface, so this host cannot attach it safely";
517
518    fn x11_parent_id(parent: RawWindowHandle) -> Result<u32> {
519        match parent {
520            RawWindowHandle::Xcb(handle) => Ok(handle.window.get()),
521            RawWindowHandle::Xlib(handle) => u32::try_from(handle.window)
522                .map_err(|_| Error::Other("Xlib parent window id exceeds 32 bits".to_string())),
523            RawWindowHandle::Wayland(_) => Err(Error::Other(WAYLAND_UNSUPPORTED.to_string())),
524            _ => Err(Error::Other(
525                "expected an X11 (Xcb/Xlib) window handle for the parent".to_string(),
526            )),
527        }
528    }
529
530    /// A plugin editor embedded as a child X11 window of the host window.
531    pub struct LinuxEmbed {
532        connection: xcb::Connection,
533        child: x::Window,
534    }
535
536    impl LinuxEmbed {
537        pub fn new(
538            plugin: &Arc<Mutex<Plugin>>,
539            parent: RawWindowHandle,
540            rect: EditorRect,
541        ) -> Result<Self> {
542            let parent_id = x11_parent_id(parent)?;
543
544            let (connection, screen_number) = xcb::Connection::connect(None)
545                .map_err(|e| Error::Other(format!("Failed to connect to X server: {e}")))?;
546            let visual = {
547                let setup = connection.get_setup();
548                let screen = setup
549                    .roots()
550                    .nth(screen_number as usize)
551                    .ok_or_else(|| Error::Other("No X11 screen found".to_string()))?;
552                screen.root_visual()
553            };
554            // `parent_id` is a live X11 window id from the host's RawWindowHandle.
555            let parent_win: x::Window = x::Window::new(parent_id);
556            let child = connection.generate_id();
557
558            connection
559                .send_and_check_request(&x::CreateWindow {
560                    depth: x::COPY_FROM_PARENT as u8,
561                    wid: child,
562                    parent: parent_win,
563                    x: rect.x as i16,
564                    y: rect.y as i16,
565                    width: (rect.width as u16).max(1),
566                    height: (rect.height as u16).max(1),
567                    border_width: 0,
568                    class: x::WindowClass::InputOutput,
569                    visual,
570                    value_list: &[x::Cw::EventMask(x::EventMask::EXPOSURE)],
571                })
572                .map_err(|e| Error::Other(format!("Failed to create X11 child window: {e}")))?;
573            connection.send_request(&x::MapWindow { window: child });
574            let _ = connection.flush();
575
576            let handle = crate::plugin::WindowHandle::from_x11(child.resource_id());
577            if let Err(e) = plugin
578                .lock()
579                .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
580                .open_editor(handle)
581            {
582                connection.send_request(&x::DestroyWindow { window: child });
583                let _ = connection.flush();
584                return Err(e);
585            }
586
587            Ok(Self { connection, child })
588        }
589
590        pub fn set_rect(&self, rect: EditorRect) {
591            self.connection.send_request(&x::ConfigureWindow {
592                window: self.child,
593                value_list: &[
594                    x::ConfigWindow::X(rect.x as i32),
595                    x::ConfigWindow::Y(rect.y as i32),
596                    x::ConfigWindow::Width((rect.width as u32).max(1)),
597                    x::ConfigWindow::Height((rect.height as u32).max(1)),
598                ],
599            });
600            let _ = self.connection.flush();
601        }
602    }
603
604    impl Drop for LinuxEmbed {
605        fn drop(&mut self) {
606            self.connection
607                .send_request(&x::DestroyWindow { window: self.child });
608            let _ = self.connection.flush();
609        }
610    }
611
612    #[cfg(test)]
613    mod tests {
614        use super::*;
615        use raw_window_handle::{WaylandWindowHandle, XcbWindowHandle};
616        use std::num::NonZeroU32;
617        use std::ptr::NonNull;
618
619        #[test]
620        fn accepts_x11_parent_without_touching_the_x_server() {
621            let handle = XcbWindowHandle::new(NonZeroU32::new(73).unwrap());
622            assert_eq!(x11_parent_id(RawWindowHandle::Xcb(handle)).unwrap(), 73);
623        }
624
625        #[test]
626        fn rejects_wayland_surface_with_actionable_contract_error() {
627            let surface = NonNull::<u8>::dangling().cast();
628            let handle = WaylandWindowHandle::new(surface);
629            let error = x11_parent_id(RawWindowHandle::Wayland(handle)).unwrap_err();
630            assert!(error.to_string().contains("IWaylandHost/IWaylandFrame"));
631            assert!(error.to_string().contains("RawWindowHandle"));
632        }
633    }
634}