Skip to main content

vst3_host/
window.rs

1//! Plugin window management
2//!
3//! This module provides platform-specific window creation and management
4//! for VST3 plugin GUIs.
5
6use crate::error::{Error, Result};
7use crate::plugin::Plugin;
8use std::sync::{Arc, Mutex};
9
10#[cfg(target_os = "macos")]
11use objc2::{rc::Retained, MainThreadMarker, MainThreadOnly};
12#[cfg(target_os = "macos")]
13use objc2_app_kit::{NSBackingStoreType, NSView, NSWindow, NSWindowStyleMask};
14#[cfg(target_os = "macos")]
15use objc2_foundation::{NSPoint, NSRect, NSSize, NSString};
16
17#[cfg(target_os = "windows")]
18use winapi::{
19    shared::minwindef::{HINSTANCE, LPARAM, LRESULT, UINT, WPARAM},
20    shared::windef::{HWND, RECT},
21    um::libloaderapi::GetModuleHandleW,
22    um::winuser::{
23        CreateWindowExW, DefWindowProcW, DestroyWindow, LoadCursorW, RegisterClassExW, ShowWindow,
24        UpdateWindow, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, IDC_ARROW, SW_SHOW, WNDCLASSEXW,
25        WS_OVERLAPPEDWINDOW,
26    },
27};
28
29/// An X11 window (connection + window id) backing a plugin editor on Linux.
30///
31/// Ported from the khremeviuc1004 fork's XCB implementation.
32#[cfg(target_os = "linux")]
33struct XcbWindowState {
34    connection: xcb::Connection,
35    window: xcb::x::Window,
36}
37
38/// A plugin window that manages the native window and plugin editor lifecycle
39pub struct PluginWindow {
40    plugin: Arc<Mutex<Plugin>>,
41    #[cfg(target_os = "macos")]
42    native_window: Option<Retained<NSWindow>>,
43    #[cfg(target_os = "windows")]
44    native_window: Option<HWND>,
45    #[cfg(target_os = "linux")]
46    native_window: Option<XcbWindowState>,
47}
48
49impl PluginWindow {
50    /// Create a new plugin window for the given plugin
51    pub fn new(plugin: Arc<Mutex<Plugin>>) -> Self {
52        Self {
53            plugin,
54            #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
55            native_window: None,
56        }
57    }
58
59    /// Open the plugin window
60    pub fn open(&mut self) -> Result<()> {
61        // Check if plugin has editor
62        let has_editor = self
63            .plugin
64            .lock()
65            .unwrap_or_else(|p| p.into_inner())
66            .has_editor();
67        if !has_editor {
68            return Err(Error::Other(
69                "Plugin does not have a GUI editor".to_string(),
70            ));
71        }
72
73        // Close existing window if any
74        if self.is_open() {
75            self.close();
76        }
77
78        // Get plugin info for window title
79        let plugin_info = self
80            .plugin
81            .lock()
82            .unwrap_or_else(|p| p.into_inner())
83            .info()
84            .clone();
85
86        // Try to get editor size
87        let (width, height) = self
88            .plugin
89            .lock()
90            .unwrap_or_else(|p| p.into_inner())
91            .get_editor_size()
92            .unwrap_or((800, 600));
93
94        // Create native window
95        #[cfg(target_os = "macos")]
96        {
97            // AppKit objects must be created on the main thread.
98            let mtm = MainThreadMarker::new().ok_or_else(|| {
99                Error::Other("plugin editor window must be opened on the main thread".to_string())
100            })?;
101
102            let frame = NSRect::new(
103                NSPoint::new(100.0, 100.0),
104                NSSize::new(width as f64, height as f64),
105            );
106            let style = NSWindowStyleMask::Titled
107                | NSWindowStyleMask::Closable
108                | NSWindowStyleMask::Miniaturizable;
109
110            // SAFETY: standard AppKit window/view construction on the main thread.
111            let window = unsafe {
112                NSWindow::initWithContentRect_styleMask_backing_defer(
113                    NSWindow::alloc(mtm),
114                    frame,
115                    style,
116                    NSBackingStoreType::Buffered,
117                    false,
118                )
119            };
120
121            // Programmatic NSWindows default to `releasedWhenClosed = YES`; closing one would
122            // then release it while our `Retained<NSWindow>` also releases on drop — a
123            // double-free that crashes on close. We own the lifetime, so opt out.
124            // SAFETY: standard AppKit setter on the main thread.
125            unsafe { window.setReleasedWhenClosed(false) };
126
127            let title = NSString::from_str(&format!("{} - VST3", plugin_info.name));
128            window.setTitle(&title);
129
130            // A container view, sized to the editor, that the plugin attaches its view into.
131            let container_frame = NSRect::new(
132                NSPoint::new(0.0, 0.0),
133                NSSize::new(width as f64, height as f64),
134            );
135            let container_view = NSView::initWithFrame(NSView::alloc(mtm), container_frame);
136            if let Some(content_view) = window.contentView() {
137                content_view.addSubview(&container_view);
138            }
139
140            // Hand the plugin the container NSView to embed its editor in.
141            let window_handle = crate::plugin::WindowHandle::from_nsview(Retained::as_ptr(
142                &container_view,
143            )
144                as *mut std::ffi::c_void);
145            self.plugin
146                .lock()
147                .unwrap_or_else(|p| p.into_inner())
148                .open_editor(window_handle)?;
149
150            // Match the window to the editor size, then show and center it.
151            window.setContentSize(container_frame.size);
152            window.makeKeyAndOrderFront(None);
153            window.center();
154
155            self.native_window = Some(window);
156        }
157
158        #[cfg(target_os = "windows")]
159        {
160            unsafe {
161                use std::mem;
162                use std::ptr;
163
164                // Register window class if not already registered
165                let class_name = "VST3PluginWindow\0".encode_utf16().collect::<Vec<u16>>();
166                let mut wc: WNDCLASSEXW = mem::zeroed();
167                wc.cbSize = mem::size_of::<WNDCLASSEXW>() as UINT;
168                wc.style = CS_HREDRAW | CS_VREDRAW;
169                wc.lpfnWndProc = Some(DefWindowProcW);
170                wc.hInstance = GetModuleHandleW(ptr::null());
171                wc.hCursor = LoadCursorW(ptr::null_mut(), IDC_ARROW);
172                wc.lpszClassName = class_name.as_ptr();
173
174                // Try to register, ignore if already registered
175                RegisterClassExW(&wc);
176
177                // Create window
178                let window_title = format!("{} - VST3\0", plugin_info.name);
179                let window_name = window_title.encode_utf16().collect::<Vec<u16>>();
180
181                // Calculate window size including borders
182                let mut rect = RECT {
183                    left: 0,
184                    top: 0,
185                    right: width,
186                    bottom: height,
187                };
188
189                winapi::um::winuser::AdjustWindowRectEx(
190                    &mut rect,
191                    WS_OVERLAPPEDWINDOW,
192                    0, // No menu
193                    0, // No extended style
194                );
195
196                let window_width = rect.right - rect.left;
197                let window_height = rect.bottom - rect.top;
198
199                let hwnd = CreateWindowExW(
200                    0,
201                    class_name.as_ptr(),
202                    window_name.as_ptr(),
203                    WS_OVERLAPPEDWINDOW,
204                    CW_USEDEFAULT,
205                    CW_USEDEFAULT,
206                    window_width,
207                    window_height,
208                    ptr::null_mut(),
209                    ptr::null_mut(),
210                    GetModuleHandleW(ptr::null()),
211                    ptr::null_mut(),
212                );
213
214                if hwnd.is_null() {
215                    return Err(Error::Other("Failed to create native window".to_string()));
216                }
217
218                // Try to open plugin editor
219                let window_handle =
220                    crate::plugin::WindowHandle::from_hwnd(hwnd as *mut std::ffi::c_void);
221                match self
222                    .plugin
223                    .lock()
224                    .unwrap_or_else(|p| p.into_inner())
225                    .open_editor(window_handle)
226                {
227                    Ok(()) => {
228                        ShowWindow(hwnd, SW_SHOW);
229                        UpdateWindow(hwnd);
230                        self.native_window = Some(hwnd);
231                    }
232                    Err(e) => {
233                        DestroyWindow(hwnd);
234                        return Err(e);
235                    }
236                }
237            }
238        }
239
240        #[cfg(target_os = "linux")]
241        {
242            use xcb::Xid;
243
244            // Create an X11 window via XCB and embed the plugin editor into it using the
245            // VST3 X11EmbedWindowID platform type (handled in plugin_impl::open_editor).
246            let (connection, screen_number) = xcb::Connection::connect(None)
247                .map_err(|e| Error::Other(format!("Failed to connect to X server: {e}")))?;
248            let setup = connection.get_setup();
249            let screen = setup
250                .roots()
251                .nth(screen_number as usize)
252                .ok_or_else(|| Error::Other("No X11 screen found".to_string()))?;
253            let window = connection.generate_id();
254
255            connection
256                .send_and_check_request(&xcb::x::CreateWindow {
257                    depth: xcb::x::COPY_FROM_PARENT as u8,
258                    wid: window,
259                    parent: screen.root(),
260                    x: 0,
261                    y: 0,
262                    width: width as u16,
263                    height: height as u16,
264                    border_width: 0,
265                    class: xcb::x::WindowClass::InputOutput,
266                    visual: screen.root_visual(),
267                    value_list: &[
268                        xcb::x::Cw::BackPixel(screen.white_pixel()),
269                        xcb::x::Cw::EventMask(
270                            xcb::x::EventMask::EXPOSURE | xcb::x::EventMask::KEY_PRESS,
271                        ),
272                    ],
273                })
274                .map_err(|e| Error::Other(format!("Failed to create X11 window: {e}")))?;
275
276            // Window title.
277            let title = format!("{} - VST3", plugin_info.name);
278            connection.send_request(&xcb::x::ChangeProperty {
279                mode: xcb::x::PropMode::Replace,
280                window,
281                property: xcb::x::ATOM_WM_NAME,
282                r#type: xcb::x::ATOM_STRING,
283                data: title.as_bytes(),
284            });
285
286            // Show the window, then attach the plugin editor to its X11 id.
287            connection.send_request(&xcb::x::MapWindow { window });
288            let _ = connection.flush();
289
290            let handle = crate::plugin::WindowHandle::from_x11(window.resource_id());
291            self.plugin
292                .lock()
293                .unwrap_or_else(|p| p.into_inner())
294                .open_editor(handle)?;
295
296            self.native_window = Some(XcbWindowState { connection, window });
297        }
298
299        Ok(())
300    }
301
302    /// Close the plugin window
303    pub fn close(&mut self) {
304        // Close the plugin editor first
305        if let Ok(mut plugin) = self.plugin.lock() {
306            let _ = plugin.close_editor();
307        }
308
309        // Then close the native window
310        #[cfg(target_os = "macos")]
311        {
312            if let Some(window) = self.native_window.take() {
313                window.close();
314            }
315        }
316
317        #[cfg(target_os = "windows")]
318        {
319            if let Some(hwnd) = self.native_window.take() {
320                unsafe {
321                    DestroyWindow(hwnd);
322                }
323            }
324        }
325
326        #[cfg(target_os = "linux")]
327        {
328            if let Some(state) = self.native_window.take() {
329                state.connection.send_request(&xcb::x::UnmapWindow {
330                    window: state.window,
331                });
332                state.connection.send_request(&xcb::x::DestroyWindow {
333                    window: state.window,
334                });
335                let _ = state.connection.flush();
336            }
337        }
338    }
339
340    /// Check if the window is currently open
341    pub fn is_open(&self) -> bool {
342        self.native_window.is_some()
343    }
344}
345
346impl Drop for PluginWindow {
347    fn drop(&mut self) {
348        self.close();
349    }
350}
351
352/// Builder for creating plugin windows with egui integration
353#[cfg(feature = "egui-widgets")]
354pub struct PluginWindowBuilder {
355    plugin: Arc<Mutex<Plugin>>,
356}
357
358#[cfg(feature = "egui-widgets")]
359impl PluginWindowBuilder {
360    /// Create a new builder for the given plugin
361    pub fn new(plugin: Arc<Mutex<Plugin>>) -> Self {
362        Self { plugin }
363    }
364
365    /// Build and open a standalone plugin window
366    pub fn open_standalone(&self) -> Result<PluginWindow> {
367        let mut window = PluginWindow::new(self.plugin.clone());
368        window.open()?;
369        Ok(window)
370    }
371}