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. Other platforms return an error
10//! from [`EmbeddedEditor::embed`]. Requires the `egui-widgets` feature.
11#![cfg(feature = "egui-widgets")]
12
13use crate::error::{Error, Result};
14use crate::plugin::Plugin;
15use raw_window_handle::RawWindowHandle;
16use std::sync::{Arc, Mutex};
17
18/// A rectangle in the host view's logical points, **top-left origin** (egui convention).
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct EditorRect {
21    /// Left edge, points from the window's left.
22    pub x: f32,
23    /// Top edge, points from the window's top.
24    pub y: f32,
25    /// Width in points.
26    pub width: f32,
27    /// Height in points.
28    pub height: f32,
29}
30
31/// A plugin editor embedded into a host window. Drop it (or call [`Self::close`]) to detach
32/// the editor and remove the child view.
33pub struct EmbeddedEditor {
34    plugin: Arc<Mutex<Plugin>>,
35    #[cfg(target_os = "macos")]
36    inner: macos::MacEmbed,
37    #[cfg(target_os = "windows")]
38    inner: windows::WinEmbed,
39    #[cfg(target_os = "linux")]
40    inner: linux::LinuxEmbed,
41}
42
43impl EmbeddedEditor {
44    /// Embed `plugin`'s editor as a child of `parent`, at `rect`.
45    ///
46    /// Must be called on the UI/main thread (where your event loop runs). `parent` is the
47    /// host window's handle (e.g. from `eframe::Frame::window_handle()`).
48    pub fn embed(
49        plugin: Arc<Mutex<Plugin>>,
50        parent: RawWindowHandle,
51        rect: EditorRect,
52    ) -> Result<Self> {
53        #[cfg(target_os = "macos")]
54        {
55            let inner = macos::MacEmbed::new(&plugin, parent, rect)?;
56            Ok(Self { plugin, inner })
57        }
58        #[cfg(target_os = "windows")]
59        {
60            let inner = windows::WinEmbed::new(&plugin, parent, rect)?;
61            Ok(Self { plugin, inner })
62        }
63        #[cfg(target_os = "linux")]
64        {
65            let inner = linux::LinuxEmbed::new(&plugin, parent, rect)?;
66            Ok(Self { plugin, inner })
67        }
68        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
69        {
70            let _ = (&plugin, parent, rect);
71            Err(Error::Other(
72                "editor embedding is not implemented on this platform".to_string(),
73            ))
74        }
75    }
76
77    /// Reposition/resize the embedded editor to track `rect`. Call each frame so the editor
78    /// follows the host layout (scroll, window resize). No-op off macOS.
79    pub fn set_rect(&self, rect: EditorRect) {
80        #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
81        self.inner.set_rect(rect);
82        #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
83        let _ = rect;
84    }
85
86    /// Detach the editor and remove the child view (also done on drop).
87    pub fn close(self) {}
88}
89
90impl Drop for EmbeddedEditor {
91    fn drop(&mut self) {
92        // Detach the plugin's view first; the platform child view is torn down by `inner`.
93        if let Ok(mut p) = self.plugin.lock() {
94            let _ = p.close_editor();
95        }
96    }
97}
98
99#[cfg(target_os = "macos")]
100mod macos {
101    use super::*;
102    use objc2::{rc::Retained, MainThreadMarker, MainThreadOnly};
103    use objc2_app_kit::NSView;
104    use objc2_foundation::{NSPoint, NSRect, NSSize};
105
106    pub struct MacEmbed {
107        parent: Retained<NSView>,
108        child: Retained<NSView>,
109    }
110
111    impl MacEmbed {
112        pub fn new(
113            plugin: &Arc<Mutex<Plugin>>,
114            parent: RawWindowHandle,
115            rect: EditorRect,
116        ) -> Result<Self> {
117            let mtm = MainThreadMarker::new().ok_or_else(|| {
118                Error::Other("editor embedding must run on the main thread".to_string())
119            })?;
120            let RawWindowHandle::AppKit(h) = parent else {
121                return Err(Error::Other(
122                    "expected an AppKit window handle for the parent".to_string(),
123                ));
124            };
125            // The host owns `ns_view`; retain it so it outlives our use.
126            let parent: Retained<NSView> =
127                unsafe { Retained::retain(h.ns_view.as_ptr() as *mut NSView) }
128                    .ok_or_else(|| Error::Other("null parent NSView".to_string()))?;
129
130            // Create the container child view the plugin attaches into.
131            let frame = NSRect::new(
132                NSPoint::new(rect.x as f64, 0.0),
133                NSSize::new(rect.width as f64, rect.height as f64),
134            );
135            let child = NSView::initWithFrame(NSView::alloc(mtm), frame);
136            parent.addSubview(&child);
137
138            let handle = crate::plugin::WindowHandle::from_nsview(
139                Retained::as_ptr(&child) as *mut std::ffi::c_void
140            );
141            plugin
142                .lock()
143                .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
144                .open_editor(handle)?;
145
146            let embed = Self { parent, child };
147            embed.set_rect(rect);
148            Ok(embed)
149        }
150
151        pub fn set_rect(&self, rect: EditorRect) {
152            // Convert egui's top-left origin to the parent view's coordinate space. AppKit
153            // views are bottom-left origin unless flipped, so flip Y against the parent's
154            // current height (which changes as the window resizes).
155            let flipped = self.parent.isFlipped();
156            let parent_height = self.parent.bounds().size.height;
157            let y = if flipped {
158                rect.y as f64
159            } else {
160                parent_height - (rect.y + rect.height) as f64
161            };
162            let frame = NSRect::new(
163                NSPoint::new(rect.x as f64, y),
164                NSSize::new(rect.width as f64, rect.height as f64),
165            );
166            self.child.setFrame(frame);
167        }
168    }
169
170    impl Drop for MacEmbed {
171        fn drop(&mut self) {
172            self.child.removeFromSuperview();
173        }
174    }
175}
176
177#[cfg(target_os = "windows")]
178mod windows {
179    use super::*;
180    use winapi::shared::windef::HWND;
181    use winapi::um::libloaderapi::GetModuleHandleW;
182    use winapi::um::winuser::{
183        CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassExW, SetWindowPos, ShowWindow,
184        CS_HREDRAW, CS_VREDRAW, SWP_NOZORDER, SW_SHOW, WNDCLASSEXW, WS_CHILD, WS_VISIBLE,
185    };
186
187    /// A plugin editor embedded as a child `HWND` of the host window.
188    pub struct WinEmbed {
189        child: HWND,
190    }
191
192    impl WinEmbed {
193        pub fn new(
194            plugin: &Arc<Mutex<Plugin>>,
195            parent: RawWindowHandle,
196            rect: EditorRect,
197        ) -> Result<Self> {
198            let RawWindowHandle::Win32(h) = parent else {
199                return Err(Error::Other(
200                    "expected a Win32 window handle for the parent".to_string(),
201                ));
202            };
203            unsafe {
204                let parent_hwnd = h.hwnd.get() as HWND;
205                let hinstance = GetModuleHandleW(std::ptr::null());
206
207                // Register a child window class (idempotent across calls).
208                let class_name: Vec<u16> = "VST3EmbeddedEditor\0".encode_utf16().collect();
209                let mut wc: WNDCLASSEXW = std::mem::zeroed();
210                wc.cbSize = std::mem::size_of::<WNDCLASSEXW>() as u32;
211                wc.style = CS_HREDRAW | CS_VREDRAW;
212                wc.lpfnWndProc = Some(DefWindowProcW);
213                wc.hInstance = hinstance;
214                wc.lpszClassName = class_name.as_ptr();
215                RegisterClassExW(&wc);
216
217                let child = CreateWindowExW(
218                    0,
219                    class_name.as_ptr(),
220                    std::ptr::null(),
221                    WS_CHILD | WS_VISIBLE,
222                    rect.x as i32,
223                    rect.y as i32,
224                    rect.width as i32,
225                    rect.height as i32,
226                    parent_hwnd,
227                    std::ptr::null_mut(),
228                    hinstance,
229                    std::ptr::null_mut(),
230                );
231                if child.is_null() {
232                    return Err(Error::Other("Failed to create child window".to_string()));
233                }
234
235                let handle = crate::plugin::WindowHandle::from_hwnd(child as *mut std::ffi::c_void);
236                if let Err(e) = plugin
237                    .lock()
238                    .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
239                    .open_editor(handle)
240                {
241                    DestroyWindow(child);
242                    return Err(e);
243                }
244                ShowWindow(child, SW_SHOW);
245                Ok(Self { child })
246            }
247        }
248
249        pub fn set_rect(&self, rect: EditorRect) {
250            unsafe {
251                SetWindowPos(
252                    self.child,
253                    std::ptr::null_mut(),
254                    rect.x as i32,
255                    rect.y as i32,
256                    rect.width as i32,
257                    rect.height as i32,
258                    SWP_NOZORDER,
259                );
260            }
261        }
262    }
263
264    impl Drop for WinEmbed {
265        fn drop(&mut self) {
266            unsafe {
267                DestroyWindow(self.child);
268            }
269        }
270    }
271}
272
273#[cfg(target_os = "linux")]
274mod linux {
275    use super::*;
276    use xcb::{x, Xid, XidNew};
277
278    /// A plugin editor embedded as a child X11 window of the host window.
279    pub struct LinuxEmbed {
280        connection: xcb::Connection,
281        child: x::Window,
282    }
283
284    impl LinuxEmbed {
285        pub fn new(
286            plugin: &Arc<Mutex<Plugin>>,
287            parent: RawWindowHandle,
288            rect: EditorRect,
289        ) -> Result<Self> {
290            let parent_id: u32 = match parent {
291                RawWindowHandle::Xcb(h) => h.window.get(),
292                RawWindowHandle::Xlib(h) => h.window as u32,
293                _ => {
294                    return Err(Error::Other(
295                        "expected an X11 (Xcb/Xlib) window handle for the parent".to_string(),
296                    ))
297                }
298            };
299
300            let (connection, screen_number) = xcb::Connection::connect(None)
301                .map_err(|e| Error::Other(format!("Failed to connect to X server: {e}")))?;
302            let visual = {
303                let setup = connection.get_setup();
304                let screen = setup
305                    .roots()
306                    .nth(screen_number as usize)
307                    .ok_or_else(|| Error::Other("No X11 screen found".to_string()))?;
308                screen.root_visual()
309            };
310            // `parent_id` is a live X11 window id from the host's RawWindowHandle.
311            let parent_win: x::Window = x::Window::new(parent_id);
312            let child = connection.generate_id();
313
314            connection
315                .send_and_check_request(&x::CreateWindow {
316                    depth: x::COPY_FROM_PARENT as u8,
317                    wid: child,
318                    parent: parent_win,
319                    x: rect.x as i16,
320                    y: rect.y as i16,
321                    width: (rect.width as u16).max(1),
322                    height: (rect.height as u16).max(1),
323                    border_width: 0,
324                    class: x::WindowClass::InputOutput,
325                    visual,
326                    value_list: &[x::Cw::EventMask(x::EventMask::EXPOSURE)],
327                })
328                .map_err(|e| Error::Other(format!("Failed to create X11 child window: {e}")))?;
329            connection.send_request(&x::MapWindow { window: child });
330            let _ = connection.flush();
331
332            let handle = crate::plugin::WindowHandle::from_x11(child.resource_id());
333            if let Err(e) = plugin
334                .lock()
335                .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
336                .open_editor(handle)
337            {
338                connection.send_request(&x::DestroyWindow { window: child });
339                let _ = connection.flush();
340                return Err(e);
341            }
342
343            Ok(Self { connection, child })
344        }
345
346        pub fn set_rect(&self, rect: EditorRect) {
347            self.connection.send_request(&x::ConfigureWindow {
348                window: self.child,
349                value_list: &[
350                    x::ConfigWindow::X(rect.x as i32),
351                    x::ConfigWindow::Y(rect.y as i32),
352                    x::ConfigWindow::Width((rect.width as u32).max(1)),
353                    x::ConfigWindow::Height((rect.height as u32).max(1)),
354                ],
355            });
356            let _ = self.connection.flush();
357        }
358    }
359
360    impl Drop for LinuxEmbed {
361        fn drop(&mut self) {
362            self.connection
363                .send_request(&x::DestroyWindow { window: self.child });
364            let _ = self.connection.flush();
365        }
366    }
367}