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 #[cfg(target_os = "android")]
48 native_window: Option<()>,
49}
50
51impl PluginWindow {
52 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 pub fn open(&mut self) -> Result<()> {
68 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 if self.is_open() {
82 self.close();
83 }
84
85 let plugin_info = self
87 .plugin
88 .lock()
89 .unwrap_or_else(|p| p.into_inner())
90 .info()
91 .clone();
92
93 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 #[cfg(target_os = "macos")]
103 {
104 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 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 unsafe { window.setReleasedWhenClosed(false) };
133
134 let title = NSString::from_str(&format!("{} - VST3", plugin_info.name));
135 window.setTitle(&title);
136
137 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 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 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 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 RegisterClassExW(&wc);
183
184 let window_title = format!("{} - VST3\0", plugin_info.name);
186 let window_name = window_title.encode_utf16().collect::<Vec<u16>>();
187
188 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, 0, );
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 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 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 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 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 pub fn close(&mut self) {
318 if let Ok(mut plugin) = self.plugin.lock() {
320 let _ = plugin.close_editor();
321 }
322
323 #[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 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#[cfg(feature = "egui-widgets")]
373pub struct PluginWindowBuilder {
374 plugin: Arc<Mutex<Plugin>>,
375}
376
377#[cfg(feature = "egui-widgets")]
378impl PluginWindowBuilder {
379 pub fn new(plugin: Arc<Mutex<Plugin>>) -> Self {
381 Self { plugin }
382 }
383
384 pub fn open_standalone(&self) -> Result<PluginWindow> {
386 let mut window = PluginWindow::new(self.plugin.clone());
387 window.open()?;
388 Ok(window)
389 }
390}