1#![cfg(feature = "egui-widgets")]
24
25use crate::error::{Error, Result};
26use crate::plugin::Plugin;
27use raw_window_handle::RawWindowHandle;
28use std::cell::Cell;
29use std::sync::{Arc, Mutex};
30
31#[cfg(target_os = "linux")]
34use linux::LinuxEmbed as PlatformEmbed;
35#[cfg(target_os = "macos")]
36use macos::MacEmbed as PlatformEmbed;
37#[cfg(target_os = "windows")]
38use windows::WinEmbed as PlatformEmbed;
39
40#[derive(Clone, Copy, Debug, PartialEq)]
42pub struct EditorRect {
43 pub x: f32,
45 pub y: f32,
47 pub width: f32,
49 pub height: f32,
51}
52
53#[derive(Clone, Copy)]
56struct NegotiatedSize {
57 requested: (i32, i32),
58 accepted: (i32, i32),
59}
60
61pub struct EmbeddedEditor {
67 plugin: Arc<Mutex<Plugin>>,
68 negotiated: Cell<Option<NegotiatedSize>>,
71 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
72 inner: PlatformEmbed,
73}
74
75impl EmbeddedEditor {
76 pub fn embed(
90 plugin: Arc<Mutex<Plugin>>,
91 parent: RawWindowHandle,
92 rect: EditorRect,
93 ) -> Result<Self> {
94 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
95 {
96 let inner = PlatformEmbed::new(&plugin, parent, rect)?;
97 Ok(Self::sized(plugin, inner, rect))
98 }
99 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
100 {
101 let _ = (&plugin, parent, rect);
102 Err(Error::Other(
103 "editor embedding is not implemented on this platform".to_string(),
104 ))
105 }
106 }
107
108 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
112 fn sized(plugin: Arc<Mutex<Plugin>>, inner: PlatformEmbed, rect: EditorRect) -> Self {
113 let editor = Self {
114 plugin,
115 negotiated: Cell::new(None),
116 inner,
117 };
118 if let Err(error) = editor.try_set_rect(rect) {
119 log::warn!(
120 "embedded editor kept its own size, the requested one was declined: {error} \
121 (call EmbeddedEditor::try_set_rect to read the size in effect)"
122 );
123 }
124 editor
125 }
126
127 pub fn set_rect(&self, rect: EditorRect) {
138 let _ = self.try_set_rect(rect);
139 }
140
141 pub fn try_set_rect(&self, rect: EditorRect) -> Result<EditorRect> {
152 let requested = validated_size(rect)?;
153
154 if let Some(previous) = self.negotiated.get() {
157 if previous.requested == requested {
158 return Ok(self.place(rect, previous.accepted));
159 }
160 }
161
162 self.place(rect, requested);
165
166 let outcome = self.negotiate(requested);
167 let accepted = match &outcome {
168 Ok(accepted) => *accepted,
169 Err(_) => self.fallback_size().unwrap_or(requested),
172 };
173 self.negotiated.set(Some(NegotiatedSize {
174 requested,
175 accepted,
176 }));
177
178 let placed = if accepted == requested {
179 EditorRect {
180 width: requested.0 as f32,
181 height: requested.1 as f32,
182 ..rect
183 }
184 } else {
185 self.place(rect, accepted)
186 };
187 outcome.map(|_| placed)
188 }
189
190 pub fn take_resize_request(&self) -> Option<(i32, i32)> {
202 try_lock(&self.plugin)?.take_editor_resize_request()
203 }
204
205 fn negotiate(&self, (width, height): (i32, i32)) -> Result<(i32, i32)> {
207 self.plugin
208 .lock()
209 .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
210 .resize_editor(width, height)
211 }
212
213 fn fallback_size(&self) -> Option<(i32, i32)> {
215 if let Some(previous) = self.negotiated.get() {
216 return Some(previous.accepted);
217 }
218 try_lock(&self.plugin)?.get_editor_size().ok()
219 }
220
221 fn place(&self, rect: EditorRect, size: (i32, i32)) -> EditorRect {
223 let placed = EditorRect {
224 width: size.0 as f32,
225 height: size.1 as f32,
226 ..rect
227 };
228 #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))]
231 self.inner.set_rect(placed);
232 placed
233 }
234
235 pub fn close(self) {}
237}
238
239fn validated_size(rect: EditorRect) -> Result<(i32, i32)> {
241 let finite = rect.x.is_finite()
242 && rect.y.is_finite()
243 && rect.width.is_finite()
244 && rect.height.is_finite();
245 if !finite
246 || rect.width <= 0.0
247 || rect.height <= 0.0
248 || rect.width > i32::MAX as f32
249 || rect.height > i32::MAX as f32
250 {
251 return Err(Error::Other(
252 "embedded editor rectangle must be finite with positive dimensions".to_string(),
253 ));
254 }
255 Ok((rect.width.round() as i32, rect.height.round() as i32))
256}
257
258fn try_lock(plugin: &Mutex<Plugin>) -> Option<std::sync::MutexGuard<'_, Plugin>> {
264 match plugin.try_lock() {
265 Ok(guard) => Some(guard),
266 Err(std::sync::TryLockError::Poisoned(poison)) => Some(poison.into_inner()),
267 Err(std::sync::TryLockError::WouldBlock) => None,
268 }
269}
270
271#[cfg(test)]
272mod rect_tests {
273 use super::*;
274
275 fn rect(width: f32, height: f32) -> EditorRect {
276 EditorRect {
277 x: 4.0,
278 y: 8.0,
279 width,
280 height,
281 }
282 }
283
284 #[test]
285 fn rounds_the_requested_size_to_whole_pixels() {
286 assert_eq!(validated_size(rect(799.4, 600.5)).unwrap(), (799, 601));
287 }
288
289 #[test]
290 fn rejects_sizes_a_window_system_cannot_represent() {
291 for bad in [
292 rect(0.0, 600.0),
293 rect(800.0, -1.0),
294 rect(f32::NAN, 600.0),
295 rect(800.0, f32::INFINITY),
296 ] {
297 assert!(
298 validated_size(bad).is_err(),
299 "{bad:?} should not reach the plugin"
300 );
301 }
302 }
303
304 #[test]
305 fn rejects_a_non_finite_position_even_with_a_valid_size() {
306 let mut bad = rect(800.0, 600.0);
307 bad.x = f32::NAN;
308 assert!(validated_size(bad).is_err());
309 }
310}
311
312impl Drop for EmbeddedEditor {
313 fn drop(&mut self) {
314 if let Ok(mut p) = self.plugin.lock() {
316 let _ = p.close_editor();
317 }
318 }
319}
320
321#[cfg(target_os = "macos")]
322mod macos {
323 use super::*;
324 use objc2::{rc::Retained, MainThreadMarker, MainThreadOnly};
325 use objc2_app_kit::NSView;
326 use objc2_foundation::{NSPoint, NSRect, NSSize};
327
328 pub struct MacEmbed {
329 parent: Retained<NSView>,
330 child: Retained<NSView>,
331 }
332
333 impl MacEmbed {
334 pub fn new(
335 plugin: &Arc<Mutex<Plugin>>,
336 parent: RawWindowHandle,
337 rect: EditorRect,
338 ) -> Result<Self> {
339 let mtm = MainThreadMarker::new().ok_or_else(|| {
340 Error::Other("editor embedding must run on the main thread".to_string())
341 })?;
342 let RawWindowHandle::AppKit(h) = parent else {
343 return Err(Error::Other(
344 "expected an AppKit window handle for the parent".to_string(),
345 ));
346 };
347 let parent: Retained<NSView> =
349 unsafe { Retained::retain(h.ns_view.as_ptr() as *mut NSView) }
350 .ok_or_else(|| Error::Other("null parent NSView".to_string()))?;
351
352 let frame = NSRect::new(
354 NSPoint::new(rect.x as f64, 0.0),
355 NSSize::new(rect.width as f64, rect.height as f64),
356 );
357 let child = NSView::initWithFrame(NSView::alloc(mtm), frame);
358 parent.addSubview(&child);
359
360 let handle = unsafe {
364 crate::plugin::WindowHandle::from_nsview(
365 Retained::as_ptr(&child) as *mut std::ffi::c_void
366 )
367 };
368 plugin
369 .lock()
370 .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
371 .open_editor(handle)?;
372
373 Ok(Self { parent, child })
374 }
375
376 pub fn set_rect(&self, rect: EditorRect) {
377 let flipped = self.parent.isFlipped();
381 let parent_height = self.parent.bounds().size.height;
382 let y = if flipped {
383 rect.y as f64
384 } else {
385 parent_height - (rect.y + rect.height) as f64
386 };
387 let frame = NSRect::new(
388 NSPoint::new(rect.x as f64, y),
389 NSSize::new(rect.width as f64, rect.height as f64),
390 );
391 self.child.setFrame(frame);
392 }
393 }
394
395 impl Drop for MacEmbed {
396 fn drop(&mut self) {
397 self.child.removeFromSuperview();
398 }
399 }
400}
401
402#[cfg(target_os = "windows")]
403mod windows {
404 use super::*;
405 use winapi::shared::windef::HWND;
406 use winapi::um::libloaderapi::GetModuleHandleW;
407 use winapi::um::winuser::{
408 CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassExW, SetWindowPos, ShowWindow,
409 CS_HREDRAW, CS_VREDRAW, SWP_NOZORDER, SW_SHOW, WNDCLASSEXW, WS_CHILD, WS_VISIBLE,
410 };
411
412 pub struct WinEmbed {
414 child: HWND,
415 }
416
417 impl WinEmbed {
418 pub fn new(
419 plugin: &Arc<Mutex<Plugin>>,
420 parent: RawWindowHandle,
421 rect: EditorRect,
422 ) -> Result<Self> {
423 let RawWindowHandle::Win32(h) = parent else {
424 return Err(Error::Other(
425 "expected a Win32 window handle for the parent".to_string(),
426 ));
427 };
428 unsafe {
429 let parent_hwnd = h.hwnd.get() as HWND;
430 let hinstance = GetModuleHandleW(std::ptr::null());
431
432 let class_name: Vec<u16> = "VST3EmbeddedEditor\0".encode_utf16().collect();
434 let mut wc: WNDCLASSEXW = std::mem::zeroed();
435 wc.cbSize = std::mem::size_of::<WNDCLASSEXW>() as u32;
436 wc.style = CS_HREDRAW | CS_VREDRAW;
437 wc.lpfnWndProc = Some(DefWindowProcW);
438 wc.hInstance = hinstance;
439 wc.lpszClassName = class_name.as_ptr();
440 RegisterClassExW(&wc);
441
442 let child = CreateWindowExW(
443 0,
444 class_name.as_ptr(),
445 std::ptr::null(),
446 WS_CHILD | WS_VISIBLE,
447 rect.x as i32,
448 rect.y as i32,
449 rect.width as i32,
450 rect.height as i32,
451 parent_hwnd,
452 std::ptr::null_mut(),
453 hinstance,
454 std::ptr::null_mut(),
455 );
456 if child.is_null() {
457 return Err(Error::Other("Failed to create child window".to_string()));
458 }
459
460 let handle = crate::plugin::WindowHandle::from_hwnd(child as *mut std::ffi::c_void);
463 let mut plugin = plugin
464 .lock()
465 .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?;
466 let dpi = winapi::um::winuser::GetDpiForWindow(child);
467 if dpi > 0 {
468 if let Err(error) = plugin.set_editor_scale_factor(dpi as f32 / 96.0) {
469 drop(plugin);
470 DestroyWindow(child);
471 return Err(error);
472 }
473 }
474 if let Err(e) = plugin.open_editor(handle) {
475 drop(plugin);
476 DestroyWindow(child);
477 return Err(e);
478 }
479 drop(plugin);
480 ShowWindow(child, SW_SHOW);
481 Ok(Self { child })
482 }
483 }
484
485 pub fn set_rect(&self, rect: EditorRect) {
486 unsafe {
487 SetWindowPos(
488 self.child,
489 std::ptr::null_mut(),
490 rect.x as i32,
491 rect.y as i32,
492 rect.width as i32,
493 rect.height as i32,
494 SWP_NOZORDER,
495 );
496 }
497 }
498 }
499
500 impl Drop for WinEmbed {
501 fn drop(&mut self) {
502 unsafe {
503 DestroyWindow(self.child);
504 }
505 }
506 }
507}
508
509#[cfg(target_os = "linux")]
510mod linux {
511 use super::*;
512 use xcb::{x, Xid, XidNew};
513
514 const WAYLAND_UNSUPPORTED: &str = "Wayland VST3 editor embedding requires a host compositor \
515 plus IWaylandHost/IWaylandFrame; a RawWindowHandle supplies only the system-compositor \
516 wl_surface, so this host cannot attach it safely";
517
518 fn x11_parent_id(parent: RawWindowHandle) -> Result<u32> {
519 match parent {
520 RawWindowHandle::Xcb(handle) => Ok(handle.window.get()),
521 RawWindowHandle::Xlib(handle) => u32::try_from(handle.window)
522 .map_err(|_| Error::Other("Xlib parent window id exceeds 32 bits".to_string())),
523 RawWindowHandle::Wayland(_) => Err(Error::Other(WAYLAND_UNSUPPORTED.to_string())),
524 _ => Err(Error::Other(
525 "expected an X11 (Xcb/Xlib) window handle for the parent".to_string(),
526 )),
527 }
528 }
529
530 pub struct LinuxEmbed {
532 connection: xcb::Connection,
533 child: x::Window,
534 }
535
536 impl LinuxEmbed {
537 pub fn new(
538 plugin: &Arc<Mutex<Plugin>>,
539 parent: RawWindowHandle,
540 rect: EditorRect,
541 ) -> Result<Self> {
542 let parent_id = x11_parent_id(parent)?;
543
544 let (connection, screen_number) = xcb::Connection::connect(None)
545 .map_err(|e| Error::Other(format!("Failed to connect to X server: {e}")))?;
546 let visual = {
547 let setup = connection.get_setup();
548 let screen = setup
549 .roots()
550 .nth(screen_number as usize)
551 .ok_or_else(|| Error::Other("No X11 screen found".to_string()))?;
552 screen.root_visual()
553 };
554 let parent_win: x::Window = x::Window::new(parent_id);
556 let child = connection.generate_id();
557
558 connection
559 .send_and_check_request(&x::CreateWindow {
560 depth: x::COPY_FROM_PARENT as u8,
561 wid: child,
562 parent: parent_win,
563 x: rect.x as i16,
564 y: rect.y as i16,
565 width: (rect.width as u16).max(1),
566 height: (rect.height as u16).max(1),
567 border_width: 0,
568 class: x::WindowClass::InputOutput,
569 visual,
570 value_list: &[x::Cw::EventMask(x::EventMask::EXPOSURE)],
571 })
572 .map_err(|e| Error::Other(format!("Failed to create X11 child window: {e}")))?;
573 connection.send_request(&x::MapWindow { window: child });
574 let _ = connection.flush();
575
576 let handle = crate::plugin::WindowHandle::from_x11(child.resource_id());
577 if let Err(e) = plugin
578 .lock()
579 .map_err(|_| Error::Other("plugin lock poisoned".to_string()))?
580 .open_editor(handle)
581 {
582 connection.send_request(&x::DestroyWindow { window: child });
583 let _ = connection.flush();
584 return Err(e);
585 }
586
587 Ok(Self { connection, child })
588 }
589
590 pub fn set_rect(&self, rect: EditorRect) {
591 self.connection.send_request(&x::ConfigureWindow {
592 window: self.child,
593 value_list: &[
594 x::ConfigWindow::X(rect.x as i32),
595 x::ConfigWindow::Y(rect.y as i32),
596 x::ConfigWindow::Width((rect.width as u32).max(1)),
597 x::ConfigWindow::Height((rect.height as u32).max(1)),
598 ],
599 });
600 let _ = self.connection.flush();
601 }
602 }
603
604 impl Drop for LinuxEmbed {
605 fn drop(&mut self) {
606 self.connection
607 .send_request(&x::DestroyWindow { window: self.child });
608 let _ = self.connection.flush();
609 }
610 }
611
612 #[cfg(test)]
613 mod tests {
614 use super::*;
615 use raw_window_handle::{WaylandWindowHandle, XcbWindowHandle};
616 use std::num::NonZeroU32;
617 use std::ptr::NonNull;
618
619 #[test]
620 fn accepts_x11_parent_without_touching_the_x_server() {
621 let handle = XcbWindowHandle::new(NonZeroU32::new(73).unwrap());
622 assert_eq!(x11_parent_id(RawWindowHandle::Xcb(handle)).unwrap(), 73);
623 }
624
625 #[test]
626 fn rejects_wayland_surface_with_actionable_contract_error() {
627 let surface = NonNull::<u8>::dangling().cast();
628 let handle = WaylandWindowHandle::new(surface);
629 let error = x11_parent_id(RawWindowHandle::Wayland(handle)).unwrap_err();
630 assert!(error.to_string().contains("IWaylandHost/IWaylandFrame"));
631 assert!(error.to_string().contains("RawWindowHandle"));
632 }
633 }
634}