mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-14 14:26:46 -04:00
clap-host: host embedded plugin editors on Windows
Plugins that offer no floating editor (most CLAP plugins on Windows) now get a host window of SoundCraft's: the GUI is created embedded, parented to a top-level HWND owned by the app window, sized from get_size, resized on request_resize (applied by the editor's idle on the main thread, then set_size), and torn down when the user closes the window. Floating editors work as before; other platforms still report embedded editors as unsupported. Docs: plugin-parity editor rows and the roadmap log. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
927eeb58cf
commit
7eb643fc5e
6 files changed
+330
-14
No files matched your search
Generated
+1
@@ -3482,6 +3482,7 @@ dependencies = [
|
||||
"serde",
|
||||
"soundcraft-dsp",
|
||||
"thiserror 2.0.21",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -125,6 +125,8 @@ mutated and truncated movies. Field reports from the first public week are track
|
||||
- **2026-10-12:** `soundcraft-cli convert` rejects an unparsable or zero `--rate` and an unknown `--bit-depth` (exit 1, nothing written) instead of converting with a default (#256)
|
||||
- **2026-10-12:** Timecode labels with huge fields no longer overflow: frame counts are computed in 128 bits and clamp, so `setup.current_timecode` and `session.format_time` saturate instead of failing (#248)
|
||||
- **2026-10-12:** `session.format_time` with the smallest bar number no longer fails with an internal error: the bar offset saturates (#249)
|
||||
- **2026-10-12 (plugin editors on Windows):** VST3 editors and embedded CLAP editors open in a SoundCraft host window on Windows (owned by the app window, resized on request, closed from its title bar); verified with a JUCE VST3 compressor and a CLAP effect. No readiness percentage change.
|
||||
|
||||
- **2026-10-10 (UI docking):** Tracks and Groups, Clips, MIDI Editor, Memory Locations, and Undo History use movable panels. Edit and Mix retain separate arrangements; Clips and MIDI Editor belong to Edit. Transport, plug-in, and specialist utility windows retain their existing hosts. Panel moves and reset are available as UI commands, and saved UI preferences retain arrangements. Common toolbar and track record-enable buttons share interaction, accessible labels and keyboard focus through `craft-ui`, retaining blue tool selection and red record-arm states. No readiness percentage change.
|
||||
- **2026-10-10** — Stage re-normalized to **pre-alpha** by the core-workflow gate (interface recording, MIDI recording and session interchange fail). Full re-measure against Pro Tools Ultimate 2026.4.1 and the standard
|
||||
progress-docs set (this page, target-app parity, gaps, roadmap, UI/format/hardware/plugin/
|
||||
|
||||
@@ -13,6 +13,10 @@ serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
# The Windows host window for embedded editors (a top-level HWND the plugin parents its GUI in).
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_System_LibraryLoader", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
||||
[[test]]
|
||||
name = "editor_main_thread"
|
||||
harness = false
|
||||
|
||||
+315
-9
@@ -18,7 +18,7 @@ use clap_sys::events::{
|
||||
clap_event_note, clap_event_param_value, clap_input_events, clap_output_events,
|
||||
};
|
||||
use clap_sys::ext::audio_ports::{CLAP_AUDIO_PORT_IS_MAIN, CLAP_EXT_AUDIO_PORTS, clap_audio_port_info, clap_plugin_audio_ports};
|
||||
use clap_sys::ext::gui::{CLAP_EXT_GUI, clap_host_gui, clap_plugin_gui};
|
||||
use clap_sys::ext::gui::{CLAP_EXT_GUI, clap_host_gui, clap_plugin_gui, clap_window, clap_window_handle};
|
||||
use clap_sys::ext::latency::{CLAP_EXT_LATENCY, clap_host_latency, clap_plugin_latency};
|
||||
use clap_sys::ext::log::{CLAP_EXT_LOG, CLAP_LOG_ERROR, CLAP_LOG_FATAL, CLAP_LOG_WARNING, clap_host_log, clap_log_severity};
|
||||
use clap_sys::ext::note_ports::{CLAP_EXT_NOTE_PORTS, clap_note_port_info, clap_plugin_note_ports};
|
||||
@@ -36,7 +36,7 @@ use clap_sys::version::{CLAP_VERSION, clap_version_is_compatible};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString, c_char, c_void};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
|
||||
|
||||
/// Most plugins one factory may report (hostile-input cap).
|
||||
@@ -275,7 +275,7 @@ impl Bundle {
|
||||
inst.state_ext = inst.extension::<clap_plugin_state>(CLAP_EXT_STATE);
|
||||
let gui = inst.extension::<clap_plugin_gui>(CLAP_EXT_GUI);
|
||||
inst.gui = Arc::new(GuiLink {
|
||||
inner: Mutex::new(GuiInner { plugin, gui, alive: !gui.is_null(), created: false, visible: false }),
|
||||
inner: Mutex::new(GuiInner { plugin, gui, alive: !gui.is_null(), created: false, visible: false, window: None }),
|
||||
host: Arc::clone(&inst.state),
|
||||
});
|
||||
Ok(inst)
|
||||
@@ -310,6 +310,8 @@ pub(crate) struct HostState {
|
||||
pub gui_destroyed: AtomicBool,
|
||||
pub gui_show: AtomicBool,
|
||||
pub gui_hide: AtomicBool,
|
||||
/// A size an embedded editor asked for (`width << 32 | height`; 0 = none pending).
|
||||
pub gui_resize: AtomicU64,
|
||||
/// Parameter changes the plugin reported (output events: typically edits in its GUI),
|
||||
/// waiting for the UI. Bounded by `EDIT_CAP`; the audio thread only `try_lock`s it.
|
||||
pub edits: Mutex<Vec<(u32, f64)>>,
|
||||
@@ -328,6 +330,7 @@ impl Default for HostState {
|
||||
gui_destroyed: AtomicBool::new(false),
|
||||
gui_show: AtomicBool::new(false),
|
||||
gui_hide: AtomicBool::new(false),
|
||||
gui_resize: AtomicU64::new(0),
|
||||
edits: Mutex::new(Vec::with_capacity(EDIT_CAP)),
|
||||
}
|
||||
}
|
||||
@@ -408,8 +411,16 @@ unsafe extern "C" fn host_state_mark_dirty(host: *const clap_host) {
|
||||
|
||||
unsafe extern "C" fn host_gui_resize_hints_changed(_host: *const clap_host) {}
|
||||
|
||||
unsafe extern "C" fn host_gui_request_resize(_host: *const clap_host, _width: u32, _height: u32) -> bool {
|
||||
// Floating windows belong to the plugin, which resizes them itself.
|
||||
unsafe extern "C" fn host_gui_request_resize(host: *const clap_host, width: u32, height: u32) -> bool {
|
||||
// Floating windows belong to the plugin, which resizes them itself; an embedded editor's host
|
||||
// window is resized by the editor's `idle` on the main thread.
|
||||
// SAFETY: the plugin passes back the host pointer we gave it.
|
||||
if let Some(s) = unsafe { state(host) }
|
||||
&& width > 0
|
||||
&& height > 0
|
||||
{
|
||||
s.gui_resize.store((u64::from(width) << 32) | u64::from(height), Ordering::Relaxed);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -700,6 +711,8 @@ struct GuiInner {
|
||||
alive: bool,
|
||||
created: bool,
|
||||
visible: bool,
|
||||
/// The host window of an embedded editor (`None` for floating editors).
|
||||
window: Option<hostwin::HostWindow>,
|
||||
}
|
||||
|
||||
/// The link between an instance and its editor handles. The instance may process on the audio
|
||||
@@ -715,7 +728,14 @@ pub(crate) struct GuiLink {
|
||||
impl Default for GuiLink {
|
||||
fn default() -> Self {
|
||||
GuiLink {
|
||||
inner: Mutex::new(GuiInner { plugin: std::ptr::null(), gui: std::ptr::null(), alive: false, created: false, visible: false }),
|
||||
inner: Mutex::new(GuiInner {
|
||||
plugin: std::ptr::null(),
|
||||
gui: std::ptr::null(),
|
||||
alive: false,
|
||||
created: false,
|
||||
visible: false,
|
||||
window: None,
|
||||
}),
|
||||
host: Arc::new(HostState::default()),
|
||||
}
|
||||
}
|
||||
@@ -755,6 +775,9 @@ impl GuiLink {
|
||||
// SAFETY: live plugin with a created GUI; main thread (checked).
|
||||
unsafe { show(g.plugin) };
|
||||
}
|
||||
if let Some(w) = &g.window {
|
||||
w.raise();
|
||||
}
|
||||
g.visible = true;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -762,7 +785,11 @@ impl GuiLink {
|
||||
let (Some(supported), Some(create)) = (vt.is_api_supported, vt.create) else { return Err("incomplete clap.gui extension".into()) };
|
||||
// SAFETY: live plugin; `api` is a static NUL-terminated string; main thread.
|
||||
if !unsafe { supported(g.plugin, api.as_ptr(), true) } {
|
||||
return Err("this plugin offers only an embedded editor; SoundCraft hosts floating CLAP editors".into());
|
||||
// SAFETY: as above.
|
||||
if hostwin::SUPPORTED && unsafe { supported(g.plugin, api.as_ptr(), false) } {
|
||||
return self.open_embedded(&mut g, vt, api, title);
|
||||
}
|
||||
return Err("this plugin offers only an embedded editor, which SoundCraft hosts on Windows only".into());
|
||||
}
|
||||
self.host.gui_closed.store(false, Ordering::Relaxed);
|
||||
self.host.gui_destroyed.store(false, Ordering::Relaxed);
|
||||
@@ -794,6 +821,61 @@ impl GuiLink {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Embeds the editor in a host window of ours (plugins that offer no floating editor).
|
||||
fn open_embedded(&self, g: &mut GuiInner, vt: clap_plugin_gui, api: &CStr, title: &str) -> Result<(), String> {
|
||||
let (Some(create), Some(set_parent)) = (vt.create, vt.set_parent) else { return Err("incomplete clap.gui extension".into()) };
|
||||
self.host.gui_closed.store(false, Ordering::Relaxed);
|
||||
self.host.gui_destroyed.store(false, Ordering::Relaxed);
|
||||
self.host.gui_resize.store(0, Ordering::Relaxed);
|
||||
// SAFETY: live plugin; `api` is a static NUL-terminated string; main thread; no GUI yet.
|
||||
if !unsafe { create(g.plugin, api.as_ptr(), false) } {
|
||||
return Err("the plugin could not create its editor".into());
|
||||
}
|
||||
g.created = true;
|
||||
if let Some(f) = vt.set_scale {
|
||||
// SAFETY: live plugin with a created GUI.
|
||||
unsafe { f(g.plugin, 1.0) };
|
||||
}
|
||||
let (mut w, mut h) = (640u32, 480u32);
|
||||
if let Some(f) = vt.get_size {
|
||||
let (mut a, mut b) = (0u32, 0u32);
|
||||
// SAFETY: live plugin with a created GUI; both out-pointers are valid locals.
|
||||
if unsafe { f(g.plugin, &mut a, &mut b) } && a > 0 && b > 0 {
|
||||
(w, h) = (a.min(16_384), b.min(16_384));
|
||||
}
|
||||
}
|
||||
let win = match hostwin::HostWindow::new(title, f64::from(w), f64::from(h)) {
|
||||
Ok(win) => win,
|
||||
Err(e) => {
|
||||
GuiLink::destroy(g);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let parent = clap_window { api: api.as_ptr(), specific: clap_window_handle { win32: win.parent() } };
|
||||
// SAFETY: live plugin with a created embedded GUI; `parent` names our live window and
|
||||
// outlives the call.
|
||||
if !unsafe { set_parent(g.plugin, &parent) } {
|
||||
win.close();
|
||||
GuiLink::destroy(g);
|
||||
return Err("the editor could not attach to its window".into());
|
||||
}
|
||||
g.window = Some(win);
|
||||
let shown = match vt.show {
|
||||
// SAFETY: live plugin with a created, parented GUI.
|
||||
Some(f) => unsafe { f(g.plugin) },
|
||||
None => true,
|
||||
};
|
||||
if !shown {
|
||||
GuiLink::destroy(g);
|
||||
return Err("the plugin could not show its editor".into());
|
||||
}
|
||||
if let Some(win) = &g.window {
|
||||
win.raise();
|
||||
}
|
||||
g.visible = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn destroy(g: &mut GuiInner) {
|
||||
if g.created
|
||||
&& let Some(f) = GuiLink::vt(g).and_then(|v| v.destroy)
|
||||
@@ -803,6 +885,10 @@ impl GuiLink {
|
||||
}
|
||||
g.created = false;
|
||||
g.visible = false;
|
||||
// The plugin's child window is gone; now its host window can go too.
|
||||
if let Some(w) = g.window.take() {
|
||||
w.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes (destroys) the editor.
|
||||
@@ -815,7 +901,7 @@ impl GuiLink {
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
let g = self.lock();
|
||||
g.alive && g.created && g.visible
|
||||
g.alive && g.created && g.visible && g.window.as_ref().is_none_or(hostwin::HostWindow::is_visible)
|
||||
}
|
||||
|
||||
/// Main-thread housekeeping: runs a requested `on_main_thread`, follows the plugin's
|
||||
@@ -836,7 +922,21 @@ impl GuiLink {
|
||||
}
|
||||
}
|
||||
if g.created {
|
||||
if self.host.gui_closed.swap(false, Ordering::Relaxed) {
|
||||
let resize = self.host.gui_resize.swap(0, Ordering::Relaxed);
|
||||
if resize != 0
|
||||
&& let Some(win) = &g.window
|
||||
{
|
||||
let (w, h) = ((resize >> 32) as u32, resize as u32);
|
||||
hostwin::resize(win.raw(), f64::from(w), f64::from(h));
|
||||
if let Some(f) = GuiLink::vt(&g).and_then(|v| v.set_size) {
|
||||
// SAFETY: live plugin with a created embedded GUI, main thread.
|
||||
unsafe { f(g.plugin, w, h) };
|
||||
}
|
||||
}
|
||||
if g.window.as_ref().is_some_and(|w| !w.is_visible()) {
|
||||
// The user closed the embedded editor's host window: free the editor.
|
||||
GuiLink::destroy(&mut g);
|
||||
} else if self.host.gui_closed.swap(false, Ordering::Relaxed) {
|
||||
// The user closed the floating window (or the plugin lost its GUI): free it.
|
||||
self.host.gui_destroyed.store(false, Ordering::Relaxed);
|
||||
GuiLink::destroy(&mut g);
|
||||
@@ -872,6 +972,8 @@ impl GuiLink {
|
||||
} else {
|
||||
log::warn!("clap: editor still open while its plugin is destroyed off the main thread");
|
||||
g.created = false;
|
||||
// Window calls are main-thread only: leave the host window rather than crash.
|
||||
g.window = None;
|
||||
}
|
||||
}
|
||||
g.alive = false;
|
||||
@@ -1264,3 +1366,207 @@ impl Drop for Instance {
|
||||
self.created = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- host window for embedded editors ------------------------------------------------------
|
||||
|
||||
/// The Windows host window for embedded editors: a top-level window owned by the app's active
|
||||
/// window (no taskbar button) whose client area is the plugin's parent `HWND`. Closing it only
|
||||
/// hides it; the editor link notices in `idle`, destroys the plugin's GUI, then the window.
|
||||
/// Messages are dispatched by the app's own event loop on the main thread.
|
||||
#[cfg(windows)]
|
||||
mod hostwin {
|
||||
use std::ffi::c_void;
|
||||
use std::sync::OnceLock;
|
||||
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM};
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
AdjustWindowRectEx, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DestroyWindow, GWL_EXSTYLE, GWL_STYLE,
|
||||
GetWindowLongPtrW, IDC_ARROW, IsWindow, IsWindowVisible, LoadCursorW, RegisterClassExW, SW_HIDE, SW_SHOW, SWP_NOACTIVATE, SWP_NOMOVE,
|
||||
SWP_NOZORDER, SetForegroundWindow, SetWindowPos, ShowWindow, WM_CLOSE, WNDCLASSEXW, WS_CAPTION, WS_CLIPCHILDREN, WS_OVERLAPPED, WS_SYSMENU,
|
||||
};
|
||||
|
||||
pub const SUPPORTED: bool = true;
|
||||
|
||||
/// Rust names the process's first thread "main"; the app runs its UI (and message loop) there.
|
||||
fn on_main_thread() -> bool {
|
||||
std::thread::current().name() == Some("main")
|
||||
}
|
||||
|
||||
fn wide(s: &str) -> Vec<u16> {
|
||||
s.encode_utf16().filter(|&c| c != 0).chain(Some(0)).collect()
|
||||
}
|
||||
|
||||
const STYLE: u32 = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_CLIPCHILDREN;
|
||||
|
||||
unsafe extern "system" fn wndproc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
|
||||
if msg == WM_CLOSE {
|
||||
// SAFETY: `hwnd` is the window this procedure was called for.
|
||||
unsafe { ShowWindow(hwnd, SW_HIDE) };
|
||||
return 0;
|
||||
}
|
||||
// SAFETY: default handling of a message for the window it was sent to.
|
||||
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
|
||||
}
|
||||
|
||||
/// Registers the window class once; the name lives for the whole process.
|
||||
fn class_name() -> Option<&'static [u16]> {
|
||||
static CLASS: OnceLock<Option<Vec<u16>>> = OnceLock::new();
|
||||
CLASS
|
||||
.get_or_init(|| {
|
||||
let name = wide("SoundCraftClapEditor");
|
||||
// SAFETY: null asks for this process's module handle.
|
||||
let instance = unsafe { GetModuleHandleW(std::ptr::null()) };
|
||||
let wc = WNDCLASSEXW {
|
||||
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
|
||||
style: CS_HREDRAW | CS_VREDRAW,
|
||||
lpfnWndProc: Some(wndproc),
|
||||
cbClsExtra: 0,
|
||||
cbWndExtra: 0,
|
||||
hInstance: instance,
|
||||
hIcon: std::ptr::null_mut(),
|
||||
// SAFETY: a system cursor (null instance + predefined id).
|
||||
hCursor: unsafe { LoadCursorW(std::ptr::null_mut(), IDC_ARROW) },
|
||||
hbrBackground: std::ptr::null_mut(),
|
||||
lpszMenuName: std::ptr::null(),
|
||||
lpszClassName: name.as_ptr(),
|
||||
hIconSm: std::ptr::null_mut(),
|
||||
};
|
||||
// SAFETY: a fully initialised class description whose strings outlive the call
|
||||
// (the name is kept for the process lifetime in this `OnceLock`).
|
||||
(unsafe { RegisterClassExW(&wc) } != 0).then_some(name)
|
||||
})
|
||||
.as_deref()
|
||||
}
|
||||
|
||||
/// Outer window size for a client area of `w`×`h`.
|
||||
fn outer_size(style: u32, ex_style: u32, w: f64, h: f64) -> (i32, i32) {
|
||||
let mut r = RECT { left: 0, top: 0, right: w as i32, bottom: h as i32 };
|
||||
// SAFETY: `r` is a valid, writable rect.
|
||||
unsafe { AdjustWindowRectEx(&mut r, style, 0, ex_style) };
|
||||
(r.right.saturating_sub(r.left), r.bottom.saturating_sub(r.top))
|
||||
}
|
||||
|
||||
/// Owns the window handle; destroyed in `close` (main thread only).
|
||||
pub struct HostWindow {
|
||||
hwnd: HWND,
|
||||
}
|
||||
|
||||
// SAFETY: the handle is only used on the main thread (every method checks); the struct
|
||||
// merely carries it between threads' locks.
|
||||
unsafe impl Send for HostWindow {}
|
||||
|
||||
impl HostWindow {
|
||||
fn live(&self) -> Option<HWND> {
|
||||
// SAFETY: `IsWindow` accepts any value and only reports whether it names a window.
|
||||
(on_main_thread() && unsafe { IsWindow(self.hwnd) } != 0).then_some(self.hwnd)
|
||||
}
|
||||
|
||||
pub fn new(title: &str, w: f64, h: f64) -> Result<HostWindow, String> {
|
||||
if !on_main_thread() {
|
||||
return Err("plugin editors must be opened on the main thread".into());
|
||||
}
|
||||
let class = class_name().ok_or_else(|| "cannot register the editor window class".to_string())?;
|
||||
let title = wide(title);
|
||||
let (ow, oh) = outer_size(STYLE, 0, w, h);
|
||||
// SAFETY: main thread; the active window (or null) of this thread's queue.
|
||||
let owner = unsafe { GetActiveWindow() };
|
||||
// SAFETY: registered class and NUL-terminated strings that outlive the call; the
|
||||
// owner is null or a window of this thread.
|
||||
let hwnd = unsafe {
|
||||
CreateWindowExW(
|
||||
0,
|
||||
class.as_ptr(),
|
||||
title.as_ptr(),
|
||||
STYLE,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
ow,
|
||||
oh,
|
||||
owner,
|
||||
std::ptr::null_mut(),
|
||||
GetModuleHandleW(std::ptr::null()),
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if hwnd.is_null() {
|
||||
return Err("cannot create the editor window".into());
|
||||
}
|
||||
Ok(HostWindow { hwnd })
|
||||
}
|
||||
|
||||
/// The window handle (for resizes).
|
||||
pub fn raw(&self) -> *mut c_void {
|
||||
self.hwnd
|
||||
}
|
||||
|
||||
/// The `HWND` the plugin creates its child window in.
|
||||
pub fn parent(&self) -> *mut c_void {
|
||||
self.live().unwrap_or(std::ptr::null_mut())
|
||||
}
|
||||
|
||||
pub fn raise(&self) {
|
||||
if let Some(h) = self.live() {
|
||||
// SAFETY: a live window of this thread.
|
||||
unsafe {
|
||||
ShowWindow(h, SW_SHOW);
|
||||
SetForegroundWindow(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_visible(&self) -> bool {
|
||||
// SAFETY: a live window of this thread.
|
||||
self.live().is_some_and(|h| unsafe { IsWindowVisible(h) } != 0)
|
||||
}
|
||||
|
||||
pub fn close(self) {
|
||||
if let Some(h) = self.live() {
|
||||
// SAFETY: our own live window; the plugin's GUI was destroyed first.
|
||||
unsafe { DestroyWindow(h) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resizes a host window's client area (`window` from [`HostWindow::raw`]); main thread only.
|
||||
pub fn resize(window: *mut c_void, w: f64, h: f64) {
|
||||
// SAFETY: `IsWindow` accepts any value.
|
||||
if !on_main_thread() || window.is_null() || unsafe { IsWindow(window) } == 0 {
|
||||
return;
|
||||
}
|
||||
// SAFETY: a live window of this thread; style values are read, not changed.
|
||||
let (style, ex) = unsafe { (GetWindowLongPtrW(window, GWL_STYLE) as u32, GetWindowLongPtrW(window, GWL_EXSTYLE) as u32) };
|
||||
let (ow, oh) = outer_size(style, ex, w, h);
|
||||
// SAFETY: as above; only the size changes.
|
||||
unsafe { SetWindowPos(window, std::ptr::null_mut(), 0, 0, ow, oh, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Other platforms: embedded editors are not hosted (floating ones still are).
|
||||
#[cfg(not(windows))]
|
||||
mod hostwin {
|
||||
use std::ffi::c_void;
|
||||
|
||||
pub const SUPPORTED: bool = false;
|
||||
|
||||
pub struct HostWindow;
|
||||
|
||||
impl HostWindow {
|
||||
pub fn new(_title: &str, _w: f64, _h: f64) -> Result<HostWindow, String> {
|
||||
Err("embedded plugin editors are not hosted on this platform".into())
|
||||
}
|
||||
pub fn raw(&self) -> *mut c_void {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
pub fn parent(&self) -> *mut c_void {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
pub fn raise(&self) {}
|
||||
pub fn is_visible(&self) -> bool {
|
||||
false
|
||||
}
|
||||
pub fn close(self) {}
|
||||
}
|
||||
|
||||
pub fn resize(_window: *mut c_void, _w: f64, _h: f64) {}
|
||||
}
|
||||
@@ -20,8 +20,10 @@
|
||||
//! Plugin state (`clap.state`) round-trips through [`Plugin::save_state`] /
|
||||
//! [`Plugin::load_state`] as an opaque blob. Plugin editors (`clap.gui`) open as the plugin's own
|
||||
//! floating window ([`Plugin::open_editor`], or [`Plugin::editor`] for a handle usable on the main
|
||||
//! thread while the instance processes on the audio thread); plugins that offer only embedded
|
||||
//! editors are not hosted yet, and SoundCraft's generic parameter editor still works for them.
|
||||
//! thread while the instance processes on the audio thread). Plugins that offer only an embedded
|
||||
//! editor get a host window of SoundCraft's on Windows (resized on `request_resize`, closed by
|
||||
//! its title bar); elsewhere they are not hosted yet, and SoundCraft's generic parameter editor
|
||||
//! still works for them.
|
||||
//! Parameter changes made in the plugin's GUI arrive as output events while the plugin processes
|
||||
//! and are reported by the editor's `idle`.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Plugin parity: built-in processors and plugin hosting
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first plugin checklist against Pro Tools 2026.4.1) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (plugin editors on Windows) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
Pro Tools users live in plugins: the bundled set covers everyday mixing, and third-party plugins
|
||||
cover the rest. This doc tracks both. Our plugins have our own names and are written from
|
||||
@@ -44,8 +44,8 @@ User report #152: a saved instrument level does not reach the bounce.
|
||||
| Notes / MIDI to instruments | ✅ | ✅ | But no live MIDI input (G-02) |
|
||||
| State saved in sessions, presets | ✅ | ✅ | |
|
||||
| Create/destroy off the audio thread | ✅ | ✅ | |
|
||||
| Editors: CLAP | — | 🟡 floating windows; no embedded GUI | |
|
||||
| Editors: VST3 | — | 🟡 macOS only (verified with a commercial pitch editor) | Windows/Linux views missing |
|
||||
| Editors: CLAP | — | 🟡 floating windows; embedded editors on Windows | Embedded on macOS/Linux missing |
|
||||
| Editors: VST3 | — | 🟡 macOS (verified with a commercial pitch editor) and Windows (verified with a free JUCE compressor) | Linux views missing |
|
||||
| Editors: AU | — | ❌ | Cocoa view hosting missing |
|
||||
| Multiple editor pages | ✅ | 🟡 | #6 |
|
||||
| macOS hardened-runtime entitlement for third-party code | ✅ | ❌ | #153, #149 — signed builds refuse third-party VST3 |
|
||||
@@ -58,4 +58,5 @@ User report #152: a saved instrument level does not reach the bounce.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Editors: VST3 and embedded CLAP editors on Windows |
|
||||
| 2026-10-10 | major | Created: built-in processor inventory by category and third-party hosting checklist |
|
||||
Reference in new issue
Block a user