use crate::error::{Error, Result};
use crate::plugin::Plugin;
use std::sync::{Arc, Mutex};
#[cfg(target_os = "macos")]
use objc2::{rc::Retained, MainThreadMarker, MainThreadOnly};
#[cfg(target_os = "macos")]
use objc2_app_kit::{NSBackingStoreType, NSView, NSWindow, NSWindowStyleMask};
#[cfg(target_os = "macos")]
use objc2_foundation::{NSPoint, NSRect, NSSize, NSString};
#[cfg(target_os = "windows")]
use winapi::{
shared::minwindef::{HINSTANCE, LPARAM, LRESULT, UINT, WPARAM},
shared::windef::{HWND, RECT},
um::libloaderapi::GetModuleHandleW,
um::winuser::{
CreateWindowExW, DefWindowProcW, DestroyWindow, LoadCursorW, RegisterClassExW, ShowWindow,
UpdateWindow, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, IDC_ARROW, SW_SHOW, WNDCLASSEXW,
WS_OVERLAPPEDWINDOW,
},
};
#[cfg(target_os = "linux")]
struct XcbWindowState {
connection: xcb::Connection,
window: xcb::x::Window,
}
pub struct PluginWindow {
plugin: Arc<Mutex<Plugin>>,
#[cfg(target_os = "macos")]
native_window: Option<Retained<NSWindow>>,
#[cfg(target_os = "windows")]
native_window: Option<HWND>,
#[cfg(target_os = "linux")]
native_window: Option<XcbWindowState>,
}
impl PluginWindow {
pub fn new(plugin: Arc<Mutex<Plugin>>) -> Self {
Self {
plugin,
#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
native_window: None,
}
}
pub fn open(&mut self) -> Result<()> {
let has_editor = self.plugin.lock().unwrap().has_editor();
if !has_editor {
return Err(Error::Other(
"Plugin does not have a GUI editor".to_string(),
));
}
if self.is_open() {
self.close();
}
let plugin_info = self.plugin.lock().unwrap().info().clone();
let (width, height) = self
.plugin
.lock()
.unwrap()
.get_editor_size()
.unwrap_or((800, 600));
#[cfg(target_os = "macos")]
{
let mtm = MainThreadMarker::new().ok_or_else(|| {
Error::Other("plugin editor window must be opened on the main thread".to_string())
})?;
let frame = NSRect::new(
NSPoint::new(100.0, 100.0),
NSSize::new(width as f64, height as f64),
);
let style = NSWindowStyleMask::Titled
| NSWindowStyleMask::Closable
| NSWindowStyleMask::Miniaturizable;
let window = unsafe {
NSWindow::initWithContentRect_styleMask_backing_defer(
NSWindow::alloc(mtm),
frame,
style,
NSBackingStoreType::Buffered,
false,
)
};
let title = NSString::from_str(&format!("{} - VST3", plugin_info.name));
window.setTitle(&title);
let container_frame = NSRect::new(
NSPoint::new(0.0, 0.0),
NSSize::new(width as f64, height as f64),
);
let container_view = NSView::initWithFrame(NSView::alloc(mtm), container_frame);
if let Some(content_view) = window.contentView() {
content_view.addSubview(&container_view);
}
let window_handle = crate::plugin::WindowHandle::from_nsview(Retained::as_ptr(
&container_view,
)
as *mut std::ffi::c_void);
self.plugin.lock().unwrap().open_editor(window_handle)?;
window.setContentSize(container_frame.size);
window.makeKeyAndOrderFront(None);
window.center();
self.native_window = Some(window);
}
#[cfg(target_os = "windows")]
{
unsafe {
use std::mem;
use std::ptr;
let class_name = "VST3PluginWindow\0".encode_utf16().collect::<Vec<u16>>();
let mut wc: WNDCLASSEXW = mem::zeroed();
wc.cbSize = mem::size_of::<WNDCLASSEXW>() as UINT;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = Some(DefWindowProcW);
wc.hInstance = GetModuleHandleW(ptr::null());
wc.hCursor = LoadCursorW(ptr::null_mut(), IDC_ARROW);
wc.lpszClassName = class_name.as_ptr();
RegisterClassExW(&wc);
let window_title = format!("{} - VST3\0", plugin_info.name);
let window_name = window_title.encode_utf16().collect::<Vec<u16>>();
let mut rect = RECT {
left: 0,
top: 0,
right: width,
bottom: height,
};
winapi::um::winuser::AdjustWindowRectEx(
&mut rect,
WS_OVERLAPPEDWINDOW,
0, 0, );
let window_width = rect.right - rect.left;
let window_height = rect.bottom - rect.top;
let hwnd = CreateWindowExW(
0,
class_name.as_ptr(),
window_name.as_ptr(),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
window_width,
window_height,
ptr::null_mut(),
ptr::null_mut(),
GetModuleHandleW(ptr::null()),
ptr::null_mut(),
);
if hwnd.is_null() {
return Err(Error::Other("Failed to create native window".to_string()));
}
let window_handle =
crate::plugin::WindowHandle::from_hwnd(hwnd as *mut std::ffi::c_void);
match self.plugin.lock().unwrap().open_editor(window_handle) {
Ok(()) => {
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
self.native_window = Some(hwnd);
}
Err(e) => {
DestroyWindow(hwnd);
return Err(e);
}
}
}
}
#[cfg(target_os = "linux")]
{
use xcb::Xid;
let (connection, screen_number) = xcb::Connection::connect(None)
.map_err(|e| Error::Other(format!("Failed to connect to X server: {e}")))?;
let setup = connection.get_setup();
let screen = setup
.roots()
.nth(screen_number as usize)
.ok_or_else(|| Error::Other("No X11 screen found".to_string()))?;
let window = connection.generate_id();
connection
.send_and_check_request(&xcb::x::CreateWindow {
depth: xcb::x::COPY_FROM_PARENT as u8,
wid: window,
parent: screen.root(),
x: 0,
y: 0,
width: width as u16,
height: height as u16,
border_width: 0,
class: xcb::x::WindowClass::InputOutput,
visual: screen.root_visual(),
value_list: &[
xcb::x::Cw::BackPixel(screen.white_pixel()),
xcb::x::Cw::EventMask(
xcb::x::EventMask::EXPOSURE | xcb::x::EventMask::KEY_PRESS,
),
],
})
.map_err(|e| Error::Other(format!("Failed to create X11 window: {e}")))?;
let title = format!("{} - VST3", plugin_info.name);
connection.send_request(&xcb::x::ChangeProperty {
mode: xcb::x::PropMode::Replace,
window,
property: xcb::x::ATOM_WM_NAME,
r#type: xcb::x::ATOM_STRING,
data: title.as_bytes(),
});
connection.send_request(&xcb::x::MapWindow { window });
let _ = connection.flush();
let handle = crate::plugin::WindowHandle::from_x11(window.resource_id());
self.plugin.lock().unwrap().open_editor(handle)?;
self.native_window = Some(XcbWindowState { connection, window });
}
Ok(())
}
pub fn close(&mut self) {
if let Ok(mut plugin) = self.plugin.lock() {
let _ = plugin.close_editor();
}
#[cfg(target_os = "macos")]
{
if let Some(window) = self.native_window.take() {
window.close();
}
}
#[cfg(target_os = "windows")]
{
if let Some(hwnd) = self.native_window.take() {
unsafe {
DestroyWindow(hwnd);
}
}
}
#[cfg(target_os = "linux")]
{
if let Some(state) = self.native_window.take() {
state.connection.send_request(&xcb::x::UnmapWindow {
window: state.window,
});
state.connection.send_request(&xcb::x::DestroyWindow {
window: state.window,
});
let _ = state.connection.flush();
}
}
}
pub fn is_open(&self) -> bool {
self.native_window.is_some()
}
}
impl Drop for PluginWindow {
fn drop(&mut self) {
self.close();
}
}
#[cfg(feature = "egui-widgets")]
pub struct PluginWindowBuilder {
plugin: Arc<Mutex<Plugin>>,
}
#[cfg(feature = "egui-widgets")]
impl PluginWindowBuilder {
pub fn new(plugin: Arc<Mutex<Plugin>>) -> Self {
Self { plugin }
}
pub fn open_standalone(&self) -> Result<PluginWindow> {
let mut window = PluginWindow::new(self.plugin.clone());
window.open()?;
Ok(window)
}
}