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