1use 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#[cfg(target_os = "linux")]
33struct XcbWindowState {
34 connection: xcb::Connection,
35 window: xcb::x::Window,
36}
37
38pub 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 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 pub fn open(&mut self) -> Result<()> {
61 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 if self.is_open() {
75 self.close();
76 }
77
78 let plugin_info = self
80 .plugin
81 .lock()
82 .unwrap_or_else(|p| p.into_inner())
83 .info()
84 .clone();
85
86 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 #[cfg(target_os = "macos")]
96 {
97 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 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 unsafe { window.setReleasedWhenClosed(false) };
126
127 let title = NSString::from_str(&format!("{} - VST3", plugin_info.name));
128 window.setTitle(&title);
129
130 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 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 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 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 RegisterClassExW(&wc);
176
177 let window_title = format!("{} - VST3\0", plugin_info.name);
179 let window_name = window_title.encode_utf16().collect::<Vec<u16>>();
180
181 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, 0, );
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 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 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 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 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 pub fn close(&mut self) {
304 if let Ok(mut plugin) = self.plugin.lock() {
306 let _ = plugin.close_editor();
307 }
308
309 #[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 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#[cfg(feature = "egui-widgets")]
354pub struct PluginWindowBuilder {
355 plugin: Arc<Mutex<Plugin>>,
356}
357
358#[cfg(feature = "egui-widgets")]
359impl PluginWindowBuilder {
360 pub fn new(plugin: Arc<Mutex<Plugin>>) -> Self {
362 Self { plugin }
363 }
364
365 pub fn open_standalone(&self) -> Result<PluginWindow> {
367 let mut window = PluginWindow::new(self.plugin.clone());
368 window.open()?;
369 Ok(window)
370 }
371}