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::{NSApplication, 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::{LPARAM, LRESULT, UINT, WPARAM},
20 shared::windef::{HWND, RECT},
21 um::libloaderapi::GetModuleHandleW,
22 um::winuser::{
23 CreateWindowExW, DefWindowProcW, DestroyWindow, LoadCursorW, RegisterClassExW,
24 SetWindowPos, ShowWindow, UpdateWindow, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, IDC_ARROW,
25 SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOZORDER, SW_SHOW, WM_CLOSE, WM_DPICHANGED, WNDCLASSEXW,
26 WS_OVERLAPPEDWINDOW,
27 },
28};
29
30#[cfg(any(test, target_os = "windows"))]
31fn dpi_scale_factor(dpi: u32) -> Option<f32> {
32 (dpi > 0).then_some(dpi as f32 / 96.0)
33}
34
35#[cfg(target_os = "windows")]
42fn close_requests() -> &'static Mutex<std::collections::HashSet<usize>> {
43 static REQUESTS: std::sync::OnceLock<Mutex<std::collections::HashSet<usize>>> =
44 std::sync::OnceLock::new();
45 REQUESTS.get_or_init(Mutex::default)
46}
47
48#[cfg(target_os = "windows")]
53fn dpi_changes() -> &'static Mutex<std::collections::HashMap<usize, u32>> {
54 static CHANGES: std::sync::OnceLock<Mutex<std::collections::HashMap<usize, u32>>> =
55 std::sync::OnceLock::new();
56 CHANGES.get_or_init(Mutex::default)
57}
58
59#[cfg(target_os = "windows")]
60fn dpi_from_wparam(wparam: WPARAM) -> Option<u32> {
61 let dpi = (wparam & 0xffff) as u32;
64 (dpi > 0).then_some(dpi)
65}
66
67#[cfg(target_os = "windows")]
75unsafe extern "system" fn plugin_window_proc(
76 hwnd: HWND,
77 msg: UINT,
78 wparam: WPARAM,
79 lparam: LPARAM,
80) -> LRESULT {
81 if msg == WM_CLOSE {
82 if let Ok(mut requests) = close_requests().lock() {
83 requests.insert(hwnd as usize);
84 }
85 return 0;
86 }
87 if msg == WM_DPICHANGED {
88 if let Some(suggested) = (lparam as *const RECT).as_ref() {
92 SetWindowPos(
93 hwnd,
94 std::ptr::null_mut(),
95 suggested.left,
96 suggested.top,
97 suggested.right - suggested.left,
98 suggested.bottom - suggested.top,
99 SWP_NOZORDER | SWP_NOACTIVATE,
100 );
101 }
102 if let Some(dpi) = dpi_from_wparam(wparam) {
103 if let Ok(mut changes) = dpi_changes().lock() {
104 changes.insert(hwnd as usize, dpi);
105 }
106 }
107 return 0;
108 }
109 DefWindowProcW(hwnd, msg, wparam, lparam)
110}
111
112#[cfg(target_os = "linux")]
116struct XcbWindowState {
117 connection: xcb::Connection,
118 window: xcb::x::Window,
119}
120
121pub struct PluginWindow {
123 plugin: Arc<Mutex<Plugin>>,
124 #[cfg(target_os = "macos")]
125 native_window: Option<Retained<NSWindow>>,
126 #[cfg(target_os = "macos")]
129 container_view: Option<Retained<NSView>>,
130 #[cfg(target_os = "windows")]
131 native_window: Option<HWND>,
132 #[cfg(target_os = "linux")]
133 native_window: Option<XcbWindowState>,
134 #[cfg(target_os = "android")]
135 native_window: Option<()>,
136}
137
138impl PluginWindow {
139 pub fn new(plugin: Arc<Mutex<Plugin>>) -> Self {
141 Self {
142 plugin,
143 #[cfg(any(
144 target_os = "macos",
145 target_os = "windows",
146 target_os = "linux",
147 target_os = "android"
148 ))]
149 native_window: None,
150 #[cfg(target_os = "macos")]
151 container_view: None,
152 }
153 }
154
155 pub fn open(&mut self) -> Result<()> {
157 let has_editor = self
159 .plugin
160 .lock()
161 .unwrap_or_else(|p| p.into_inner())
162 .has_editor();
163 if !has_editor {
164 return Err(Error::Other(
165 "Plugin does not have a GUI editor".to_string(),
166 ));
167 }
168
169 if self.native_window.is_some() {
172 self.close();
173 }
174
175 let plugin_info = self
177 .plugin
178 .lock()
179 .unwrap_or_else(|p| p.into_inner())
180 .info()
181 .clone();
182
183 let (width, height) = self
185 .plugin
186 .lock()
187 .unwrap_or_else(|p| p.into_inner())
188 .get_editor_size()
189 .unwrap_or((800, 600));
190
191 #[cfg(target_os = "macos")]
193 {
194 let mtm = MainThreadMarker::new().ok_or_else(|| {
196 Error::Other("plugin editor window must be opened on the main thread".to_string())
197 })?;
198
199 let frame = NSRect::new(
200 NSPoint::new(100.0, 100.0),
201 NSSize::new(width as f64, height as f64),
202 );
203 let style = NSWindowStyleMask::Titled
204 | NSWindowStyleMask::Closable
205 | NSWindowStyleMask::Miniaturizable;
206
207 let window = unsafe {
209 NSWindow::initWithContentRect_styleMask_backing_defer(
210 NSWindow::alloc(mtm),
211 frame,
212 style,
213 NSBackingStoreType::Buffered,
214 false,
215 )
216 };
217
218 unsafe { window.setReleasedWhenClosed(false) };
223
224 let title = NSString::from_str(&format!("{} - VST3", plugin_info.name));
225 window.setTitle(&title);
226
227 let container_frame = NSRect::new(
229 NSPoint::new(0.0, 0.0),
230 NSSize::new(width as f64, height as f64),
231 );
232 let container_view = NSView::initWithFrame(NSView::alloc(mtm), container_frame);
233 if let Some(content_view) = window.contentView() {
234 content_view.addSubview(&container_view);
235 }
236
237 let window_handle = unsafe {
242 crate::plugin::WindowHandle::from_nsview(
243 Retained::as_ptr(&container_view) as *mut std::ffi::c_void
244 )
245 };
246 self.plugin
247 .lock()
248 .unwrap_or_else(|p| p.into_inner())
249 .open_editor(window_handle)?;
250
251 window.setContentSize(container_frame.size);
253 window.makeKeyAndOrderFront(None);
254 window.center();
255
256 self.native_window = Some(window);
257 self.container_view = Some(container_view);
258 }
259
260 #[cfg(target_os = "windows")]
261 {
262 unsafe {
263 use std::mem;
264 use std::ptr;
265
266 let class_name = "VST3PluginWindow\0".encode_utf16().collect::<Vec<u16>>();
268 let mut wc: WNDCLASSEXW = mem::zeroed();
269 wc.cbSize = mem::size_of::<WNDCLASSEXW>() as UINT;
270 wc.style = CS_HREDRAW | CS_VREDRAW;
271 wc.lpfnWndProc = Some(plugin_window_proc);
272 wc.hInstance = GetModuleHandleW(ptr::null());
273 wc.hCursor = LoadCursorW(ptr::null_mut(), IDC_ARROW);
274 wc.lpszClassName = class_name.as_ptr();
275
276 RegisterClassExW(&wc);
278
279 let window_title = format!("{} - VST3\0", plugin_info.name);
281 let window_name = window_title.encode_utf16().collect::<Vec<u16>>();
282
283 let mut rect = RECT {
285 left: 0,
286 top: 0,
287 right: width,
288 bottom: height,
289 };
290
291 winapi::um::winuser::AdjustWindowRectEx(
292 &mut rect,
293 WS_OVERLAPPEDWINDOW,
294 0, 0, );
297
298 let window_width = rect.right - rect.left;
299 let window_height = rect.bottom - rect.top;
300
301 let hwnd = CreateWindowExW(
302 0,
303 class_name.as_ptr(),
304 window_name.as_ptr(),
305 WS_OVERLAPPEDWINDOW,
306 CW_USEDEFAULT,
307 CW_USEDEFAULT,
308 window_width,
309 window_height,
310 ptr::null_mut(),
311 ptr::null_mut(),
312 GetModuleHandleW(ptr::null()),
313 ptr::null_mut(),
314 );
315
316 if hwnd.is_null() {
317 return Err(Error::Other("Failed to create native window".to_string()));
318 }
319
320 let window_handle =
324 crate::plugin::WindowHandle::from_hwnd(hwnd as *mut std::ffi::c_void);
325 let mut plugin = self.plugin.lock().unwrap_or_else(|p| p.into_inner());
326 let dpi = winapi::um::winuser::GetDpiForWindow(hwnd);
327 if let Some(scale_factor) = dpi_scale_factor(dpi) {
328 if let Err(error) = plugin.set_editor_scale_factor(scale_factor) {
329 drop(plugin);
330 DestroyWindow(hwnd);
331 return Err(error);
332 }
333 }
334 match plugin.open_editor(window_handle) {
335 Ok(()) => {
336 drop(plugin);
337 ShowWindow(hwnd, SW_SHOW);
338 UpdateWindow(hwnd);
339 self.native_window = Some(hwnd);
340 }
341 Err(e) => {
342 drop(plugin);
343 DestroyWindow(hwnd);
344 return Err(e);
345 }
346 }
347 }
348 }
349
350 #[cfg(target_os = "linux")]
351 {
352 use xcb::Xid;
353
354 let (connection, screen_number) = xcb::Connection::connect(None)
357 .map_err(|e| Error::Other(format!("Failed to connect to X server: {e}")))?;
358 let setup = connection.get_setup();
359 let screen = setup
360 .roots()
361 .nth(screen_number as usize)
362 .ok_or_else(|| Error::Other("No X11 screen found".to_string()))?;
363 let window = connection.generate_id();
364
365 connection
366 .send_and_check_request(&xcb::x::CreateWindow {
367 depth: xcb::x::COPY_FROM_PARENT as u8,
368 wid: window,
369 parent: screen.root(),
370 x: 0,
371 y: 0,
372 width: width as u16,
373 height: height as u16,
374 border_width: 0,
375 class: xcb::x::WindowClass::InputOutput,
376 visual: screen.root_visual(),
377 value_list: &[
378 xcb::x::Cw::BackPixel(screen.white_pixel()),
379 xcb::x::Cw::EventMask(
380 xcb::x::EventMask::EXPOSURE | xcb::x::EventMask::KEY_PRESS,
381 ),
382 ],
383 })
384 .map_err(|e| Error::Other(format!("Failed to create X11 window: {e}")))?;
385
386 let title = format!("{} - VST3", plugin_info.name);
388 connection.send_request(&xcb::x::ChangeProperty {
389 mode: xcb::x::PropMode::Replace,
390 window,
391 property: xcb::x::ATOM_WM_NAME,
392 r#type: xcb::x::ATOM_STRING,
393 data: title.as_bytes(),
394 });
395
396 connection.send_request(&xcb::x::MapWindow { window });
398 let _ = connection.flush();
399
400 let handle = crate::plugin::WindowHandle::from_x11(window.resource_id());
401 self.plugin
402 .lock()
403 .unwrap_or_else(|p| p.into_inner())
404 .open_editor(handle)?;
405
406 self.native_window = Some(XcbWindowState { connection, window });
407 }
408
409 #[cfg(target_os = "android")]
410 {
411 return Err(Error::Other(
412 "PluginWindow::open() is not supported on Android".to_string(),
413 ));
414 }
415
416 Ok(())
417 }
418
419 pub fn service_platform_events(&self) -> Result<()> {
439 if self.native_window.is_none() {
440 return Ok(());
441 }
442 let Some(mut plugin) = self.try_lock_plugin() else {
443 return Ok(());
444 };
445
446 let scale_result = self
447 .take_pending_scale_factor()
448 .map(|factor| plugin.set_editor_scale_factor(factor));
449 let resize = plugin.take_editor_resize_request();
450
451 drop(plugin);
454
455 if let Some((width, height)) = resize {
456 self.resize_native_window(width, height);
457 }
458 match scale_result {
459 Some(Err(error)) => Err(error),
460 _ => Ok(()),
461 }
462 }
463
464 fn try_lock_plugin(&self) -> Option<std::sync::MutexGuard<'_, Plugin>> {
468 match self.plugin.try_lock() {
469 Ok(guard) => Some(guard),
470 Err(std::sync::TryLockError::Poisoned(poison)) => Some(poison.into_inner()),
471 Err(std::sync::TryLockError::WouldBlock) => None,
472 }
473 }
474
475 #[cfg(target_os = "windows")]
477 fn take_pending_scale_factor(&self) -> Option<f32> {
478 let hwnd = self.native_window?;
479 dpi_changes()
480 .lock()
481 .unwrap_or_else(|poison| poison.into_inner())
482 .remove(&(hwnd as usize))
483 .and_then(dpi_scale_factor)
484 }
485
486 #[cfg(not(target_os = "windows"))]
488 fn take_pending_scale_factor(&self) -> Option<f32> {
489 None
490 }
491
492 fn resize_native_window(&self, width: i32, height: i32) {
497 if width <= 0 || height <= 0 {
498 return;
499 }
500 log::debug!("editor asked the host to resize its window to {width}x{height}");
501
502 #[cfg(target_os = "macos")]
503 {
504 let Some(window) = self.native_window.as_ref() else {
505 return;
506 };
507 let size = NSSize::new(width as f64, height as f64);
508 if let Some(container) = self.container_view.as_ref() {
509 container.setFrame(NSRect::new(NSPoint::new(0.0, 0.0), size));
510 }
511 window.setContentSize(size);
512 }
513
514 #[cfg(target_os = "windows")]
515 {
516 let Some(hwnd) = self.native_window else {
517 return;
518 };
519 let mut rect = RECT {
522 left: 0,
523 top: 0,
524 right: width,
525 bottom: height,
526 };
527 unsafe {
528 winapi::um::winuser::AdjustWindowRectEx(&mut rect, WS_OVERLAPPEDWINDOW, 0, 0);
529 SetWindowPos(
530 hwnd,
531 std::ptr::null_mut(),
532 0,
533 0,
534 rect.right - rect.left,
535 rect.bottom - rect.top,
536 SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE,
537 );
538 }
539 }
540
541 #[cfg(target_os = "linux")]
542 {
543 let Some(state) = self.native_window.as_ref() else {
544 return;
545 };
546 state.connection.send_request(&xcb::x::ConfigureWindow {
547 window: state.window,
548 value_list: &[
549 xcb::x::ConfigWindow::Width(width as u32),
550 xcb::x::ConfigWindow::Height(height as u32),
551 ],
552 });
553 let _ = state.connection.flush();
554 }
555
556 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
557 let _ = (width, height);
558 }
559
560 pub fn close(&mut self) {
562 let _ = self
567 .plugin
568 .lock()
569 .unwrap_or_else(|p| p.into_inner())
570 .close_editor();
571
572 #[cfg(target_os = "macos")]
574 {
575 self.container_view = None;
576 if let Some(window) = self.native_window.take() {
577 window.close();
578 }
579 }
580
581 #[cfg(target_os = "windows")]
582 {
583 if let Some(hwnd) = self.native_window.take() {
584 if let Ok(mut requests) = close_requests().lock() {
585 requests.remove(&(hwnd as usize));
586 }
587 if let Ok(mut changes) = dpi_changes().lock() {
588 changes.remove(&(hwnd as usize));
589 }
590 unsafe {
591 DestroyWindow(hwnd);
592 }
593 }
594 }
595
596 #[cfg(target_os = "linux")]
597 {
598 if let Some(state) = self.native_window.take() {
599 state.connection.send_request(&xcb::x::UnmapWindow {
600 window: state.window,
601 });
602 state.connection.send_request(&xcb::x::DestroyWindow {
603 window: state.window,
604 });
605 let _ = state.connection.flush();
606 }
607 }
608
609 #[cfg(target_os = "android")]
610 {
611 let _ = self.native_window.take();
612 }
613 }
614
615 pub fn is_open(&self) -> bool {
624 self.native_window.is_some() && !self.native_window_dismissed()
625 }
626
627 pub fn closed_by_user(&self) -> bool {
637 self.native_window_dismissed()
638 }
639
640 #[cfg(target_os = "macos")]
642 fn native_window_dismissed(&self) -> bool {
643 let Some(window) = self.native_window.as_ref() else {
644 return false;
645 };
646 if window.isVisible() || window.isMiniaturized() {
649 return false;
650 }
651 match MainThreadMarker::new() {
652 Some(mtm) => !NSApplication::sharedApplication(mtm).isHidden(),
653 None => false,
655 }
656 }
657
658 #[cfg(target_os = "windows")]
660 fn native_window_dismissed(&self) -> bool {
661 let Some(hwnd) = self.native_window else {
662 return false;
663 };
664 close_requests()
665 .lock()
666 .is_ok_and(|requests| requests.contains(&(hwnd as usize)))
667 }
668
669 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
672 fn native_window_dismissed(&self) -> bool {
673 false
674 }
675}
676
677impl Drop for PluginWindow {
678 fn drop(&mut self) {
679 self.close();
680 }
681}
682
683#[cfg(feature = "egui-widgets")]
685pub struct PluginWindowBuilder {
686 plugin: Arc<Mutex<Plugin>>,
687}
688
689#[cfg(feature = "egui-widgets")]
690impl PluginWindowBuilder {
691 pub fn new(plugin: Arc<Mutex<Plugin>>) -> Self {
693 Self { plugin }
694 }
695
696 pub fn open_standalone(&self) -> Result<PluginWindow> {
698 let mut window = PluginWindow::new(self.plugin.clone());
699 window.open()?;
700 Ok(window)
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::dpi_scale_factor;
707
708 #[test]
709 fn windows_dpi_converts_to_vst_content_scale() {
710 assert_eq!(dpi_scale_factor(0), None);
711 assert_eq!(dpi_scale_factor(96), Some(1.0));
712 assert_eq!(dpi_scale_factor(120), Some(1.25));
713 assert_eq!(dpi_scale_factor(144), Some(1.5));
714 assert_eq!(dpi_scale_factor(192), Some(2.0));
715 }
716
717 #[cfg(target_os = "windows")]
718 #[test]
719 fn wm_dpi_wparam_uses_horizontal_dpi() {
720 let packed = (192usize << 16) | 144;
721 assert_eq!(super::dpi_from_wparam(packed), Some(144));
722 assert_eq!(super::dpi_from_wparam(0), None);
723 }
724}