use std::cell::UnsafeCell;
use std::ffi::c_void;
use std::fmt::Write;
use std::sync::Arc;
use beamer_core::{GuiConstraints, GuiDelegate, ParameterStore, Size, WebViewHandler};
use beamer_webview::platform::PlatformWebView;
pub use beamer_webview::WebViewConfig;
use vst3::Steinberg::Vst::IComponentHandler;
use vst3::Steinberg::*;
use vst3::Class;
struct IpcContext {
params: *const dyn ParameterStore,
handler: *mut IComponentHandler,
webview_handler: Option<Arc<dyn WebViewHandler>>,
last_values: Vec<f64>,
webview: *const PlatformWebView,
sync_timer: *mut objc2::runtime::AnyObject,
}
pub struct WebViewPlugView {
platform: UnsafeCell<Option<PlatformWebView>>,
config: UnsafeCell<WebViewConfig<'static>>,
delegate: UnsafeCell<Box<dyn GuiDelegate>>,
size: UnsafeCell<Size>,
frame: UnsafeCell<*mut IPlugFrame>,
ipc: UnsafeCell<Box<IpcContext>>,
}
unsafe impl Send for WebViewPlugView {}
unsafe impl Sync for WebViewPlugView {}
unsafe fn handler_addref(handler: *mut IComponentHandler) {
if !handler.is_null() {
let unknown = handler as *mut FUnknown;
unsafe { ((*(*unknown).vtbl).addRef)(unknown) };
}
}
unsafe fn handler_release(handler: *mut IComponentHandler) {
if !handler.is_null() {
let unknown = handler as *mut FUnknown;
unsafe { ((*(*unknown).vtbl).release)(unknown) };
}
}
impl WebViewPlugView {
pub unsafe fn new(
config: WebViewConfig<'static>,
delegate: Box<dyn GuiDelegate>,
params: *const dyn ParameterStore,
component_handler: *mut IComponentHandler,
webview_handler: Option<Arc<dyn WebViewHandler>>,
) -> Self {
let size = delegate.gui_size();
let param_count = unsafe { &*params }.count();
let last_values = vec![f64::NAN; param_count];
unsafe { handler_addref(component_handler) };
Self {
platform: UnsafeCell::new(None),
config: UnsafeCell::new(config),
delegate: UnsafeCell::new(delegate),
size: UnsafeCell::new(size),
frame: UnsafeCell::new(std::ptr::null_mut()),
ipc: UnsafeCell::new(Box::new(IpcContext {
params,
handler: component_handler,
webview_handler,
last_values,
webview: std::ptr::null(),
sync_timer: std::ptr::null_mut(),
})),
}
}
pub unsafe fn set_component_handler(&self, handler: *mut IComponentHandler) {
let ipc = unsafe { &mut *self.ipc.get() };
let old = ipc.handler;
ipc.handler = handler;
unsafe {
handler_addref(handler);
handler_release(old);
}
}
}
impl Class for WebViewPlugView {
type Interfaces = (IPlugView,);
}
unsafe extern "C-unwind" fn on_message(context: *mut c_void, json: *const u8, len: usize) {
if context.is_null() || json.is_null() {
return;
}
let ipc = unsafe { &mut *(context as *mut IpcContext) };
let bytes = unsafe { std::slice::from_raw_parts(json, len) };
let Ok(json_str) = std::str::from_utf8(bytes) else {
log::error!("IPC message is not valid UTF-8.");
return;
};
let Ok(msg) = serde_json::from_str::<serde_json::Value>(json_str) else {
log::warn!("Invalid IPC message JSON: {json_str}");
return;
};
let Some(msg_type) = msg.get("type").and_then(|t| t.as_str()) else {
return;
};
let params = unsafe { &*ipc.params };
match msg_type {
"param:set" => {
let Some(id) = msg.get("id").and_then(|v| v.as_u64()).map(|v| v as u32) else { return };
let Some(value) = msg.get("value").and_then(|v| v.as_f64()) else { return };
params.set_normalized(id, value);
if !ipc.handler.is_null() {
unsafe {
((*(*ipc.handler).vtbl).performEdit)(ipc.handler, id, value);
}
}
if !ipc.webview.is_null() {
let norm = params.get_normalized(id);
let plain = params.normalized_to_plain(id, norm);
let text = params.normalized_to_string(id, norm);
let text_json = serde_json::to_string(&text).unwrap_or_default();
let webview = unsafe { &*ipc.webview };
webview.evaluate_js(&format!(
"window.__BEAMER__._onParams({{{}:[{},{},{}]}})",
id, norm, plain, text_json
));
for i in 0..params.count() {
if let Some(info) = params.info(i) {
if info.id == id && i < ipc.last_values.len() {
ipc.last_values[i] = norm;
break;
}
}
}
}
}
"param:begin" => {
let Some(id) = msg.get("id").and_then(|v| v.as_u64()).map(|v| v as u32) else { return };
if !ipc.handler.is_null() {
unsafe {
((*(*ipc.handler).vtbl).beginEdit)(ipc.handler, id);
}
}
}
"param:end" => {
let Some(id) = msg.get("id").and_then(|v| v.as_u64()).map(|v| v as u32) else { return };
if !ipc.handler.is_null() {
unsafe {
((*(*ipc.handler).vtbl).endEdit)(ipc.handler, id);
}
}
}
"invoke" => {
let Some(method) = msg.get("method").and_then(|v| v.as_str()) else { return };
let args = msg.get("args").and_then(|v| v.as_array()).cloned().unwrap_or_default();
let call_id = msg.get("callId").and_then(|v| v.as_u64()).unwrap_or(0);
let result = if method == "_beamer/paramTextToNormalized" {
let param_id = args.first().and_then(|v| v.as_u64()).map(|v| v as u32);
let text = args.get(1).and_then(|v| v.as_str());
match (param_id, text) {
(Some(id), Some(s)) => match params.string_to_normalized(id, s) {
Some(norm) => Ok(serde_json::Value::from(norm)),
None => Ok(serde_json::Value::Null),
},
_ => Ok(serde_json::Value::Null),
}
} else {
match &ipc.webview_handler {
Some(handler) => handler.on_invoke(method, &args),
None => Ok(serde_json::Value::Null),
}
};
if !ipc.webview.is_null() {
let webview = unsafe { &*ipc.webview };
let js = match result {
Ok(val) => {
let json = serde_json::to_string(&val).unwrap_or_else(|_| "null".into());
format!("window.__BEAMER__._onResult({call_id},{{\"ok\":{json}}})")
}
Err(err) => {
let escaped = serde_json::to_string(&err).unwrap_or_default();
format!("window.__BEAMER__._onResult({call_id},{{\"err\":{escaped}}})")
}
};
webview.evaluate_js(&js);
}
}
"event" => {
let Some(name) = msg.get("name").and_then(|v| v.as_str()) else { return };
let data = msg.get("data").cloned().unwrap_or(serde_json::Value::Null);
if let Some(handler) = &ipc.webview_handler {
handler.on_event(name, &data);
}
}
_ => {
log::debug!("Unknown IPC message type: {msg_type}");
}
}
}
unsafe extern "C-unwind" fn on_loaded(context: *mut c_void) {
if context.is_null() {
return;
}
let ipc = unsafe { &*(context as *const IpcContext) };
if ipc.webview.is_null() {
return;
}
let params = unsafe { &*ipc.params };
let webview = unsafe { &*ipc.webview };
let json_array = beamer_core::params_to_init_json(params);
let js = format!("window.__BEAMER__._onInit({json_array})");
webview.evaluate_js(&js);
}
unsafe extern "C-unwind" fn sync_timer_fired(
_this: *mut objc2::runtime::AnyObject,
_cmd: objc2::runtime::Sel,
timer: *mut objc2::runtime::AnyObject,
) {
let user_info: *mut objc2::runtime::AnyObject = unsafe { objc2::msg_send![timer, userInfo] };
if user_info.is_null() {
return;
}
let ptr: *const objc2::runtime::AnyObject = unsafe { objc2::msg_send![user_info, pointerValue] };
if ptr.is_null() {
return;
}
let ipc = unsafe { &mut *(ptr as *mut IpcContext) };
if ipc.webview.is_null() {
return;
}
let params = unsafe { &*ipc.params };
let webview = unsafe { &*ipc.webview };
let mut script = String::new();
let mut any_changed = false;
let count = params.count();
for i in 0..count {
let Some(info) = params.info(i) else { continue };
let val = params.get_normalized(info.id);
if i < ipc.last_values.len() && val != ipc.last_values[i] {
ipc.last_values[i] = val;
if !any_changed {
script.push_str("window.__BEAMER__._onParams({");
any_changed = true;
} else {
script.push(',');
}
let plain = params.normalized_to_plain(info.id, val);
let text = params.normalized_to_string(info.id, val);
let text_json = serde_json::to_string(&text).unwrap_or_default();
let _ = write!(script, "{}:[{},{},{}]", info.id, val, plain, text_json);
}
}
if any_changed {
script.push_str("})");
webview.evaluate_js(&script);
}
}
#[allow(non_snake_case)]
impl IPlugViewTrait for WebViewPlugView {
unsafe fn isPlatformTypeSupported(&self, r#type: FIDString) -> tresult {
if r#type.is_null() {
return kResultFalse;
}
let type_str = unsafe { std::ffi::CStr::from_ptr(r#type) };
#[cfg(target_os = "macos")]
let supported = type_str == unsafe { std::ffi::CStr::from_ptr(kPlatformTypeNSView) };
#[cfg(target_os = "windows")]
let supported = type_str == unsafe { std::ffi::CStr::from_ptr(kPlatformTypeHWND) };
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let supported = false;
if supported { kResultOk } else { kResultFalse }
}
unsafe fn attached(&self, parent: *mut c_void, r#type: FIDString) -> tresult {
if unsafe { self.isPlatformTypeSupported(r#type) } != kResultOk {
return kResultFalse;
}
let platform = unsafe { &mut *self.platform.get() };
if platform.is_some() {
return kResultFalse;
}
let ipc = unsafe { &mut *self.ipc.get() };
let ipc_ptr = &mut **ipc as *mut IpcContext as *mut c_void;
let config = unsafe { &mut *self.config.get() };
config.message_callback = Some(on_message);
config.loaded_callback = Some(on_loaded);
config.callback_context = ipc_ptr;
match unsafe { PlatformWebView::attach_to_parent(parent, config) } {
Ok(webview) => {
*platform = Some(webview);
ipc.webview = platform.as_ref().unwrap() as *const PlatformWebView;
for v in &mut ipc.last_values {
*v = f64::NAN;
}
#[cfg(target_os = "macos")]
{
use objc2::msg_send;
let ns_value: *mut objc2::runtime::AnyObject = unsafe {
msg_send![
objc2::runtime::AnyClass::get(c"NSValue").unwrap(),
valueWithPointer: ipc_ptr
]
};
let timer_class = get_or_register_timer_class();
let target: *mut objc2::runtime::AnyObject = unsafe {
msg_send![timer_class, alloc]
};
let target: *mut objc2::runtime::AnyObject = unsafe {
msg_send![target, init]
};
let timer: *mut objc2::runtime::AnyObject = unsafe {
msg_send![
objc2::runtime::AnyClass::get(c"NSTimer").unwrap(),
scheduledTimerWithTimeInterval: (1.0 / 60.0f64),
target: target,
selector: objc2::sel!(beamerSyncTimerFired:),
userInfo: ns_value,
repeats: true
]
};
let _: () = unsafe { msg_send![target, release] };
ipc.sync_timer = timer;
}
let delegate = unsafe { &mut *self.delegate.get() };
delegate.gui_opened();
kResultOk
}
Err(e) => {
log::error!("Failed to create WebView: {e}");
config.message_callback = None;
config.loaded_callback = None;
config.callback_context = std::ptr::null_mut();
kResultFalse
}
}
}
unsafe fn removed(&self) -> tresult {
let delegate = unsafe { &mut *self.delegate.get() };
delegate.gui_closed();
let ipc = unsafe { &mut *self.ipc.get() };
#[cfg(target_os = "macos")]
{
if !ipc.sync_timer.is_null() {
unsafe {
let _: () = objc2::msg_send![ipc.sync_timer, invalidate];
}
ipc.sync_timer = std::ptr::null_mut();
}
}
ipc.webview = std::ptr::null();
let platform = unsafe { &mut *self.platform.get() };
if let Some(webview) = platform.as_mut() {
webview.detach();
}
*platform = None;
kResultOk
}
unsafe fn onWheel(&self, _distance: f32) -> tresult {
kResultFalse
}
unsafe fn onKeyDown(&self, _key: char16, _keyCode: int16, _modifiers: int16) -> tresult {
kResultFalse
}
unsafe fn onKeyUp(&self, _key: char16, _keyCode: int16, _modifiers: int16) -> tresult {
kResultFalse
}
unsafe fn getSize(&self, size: *mut ViewRect) -> tresult {
if size.is_null() {
return kInvalidArgument;
}
let current = unsafe { *self.size.get() };
let rect = unsafe { &mut *size };
rect.left = 0;
rect.top = 0;
rect.right = current.width as i32;
rect.bottom = current.height as i32;
kResultOk
}
unsafe fn onSize(&self, newSize: *mut ViewRect) -> tresult {
if newSize.is_null() {
return kInvalidArgument;
}
let rect = unsafe { &*newSize };
let width = (rect.right - rect.left).max(0) as u32;
let height = (rect.bottom - rect.top).max(0) as u32;
let size = unsafe { &mut *self.size.get() };
size.width = width;
size.height = height;
let new_size = Size::new(width, height);
let delegate = unsafe { &mut *self.delegate.get() };
delegate.gui_resized(new_size);
let platform = unsafe { &*self.platform.get() };
if let Some(webview) = platform.as_ref() {
#[cfg(target_os = "macos")]
webview.set_frame(0, 0, width as i32, height as i32);
#[cfg(target_os = "windows")]
webview.set_bounds(0, 0, width as i32, height as i32);
}
kResultOk
}
unsafe fn onFocus(&self, _state: TBool) -> tresult {
kResultOk
}
unsafe fn setFrame(&self, frame: *mut IPlugFrame) -> tresult {
let frame_ptr = self.frame.get();
let old_frame = unsafe { *frame_ptr };
if !old_frame.is_null() {
unsafe {
let unknown = old_frame as *mut FUnknown;
((*(*unknown).vtbl).release)(unknown);
};
}
if !frame.is_null() {
unsafe {
let unknown = frame as *mut FUnknown;
((*(*unknown).vtbl).addRef)(unknown);
};
}
unsafe { *frame_ptr = frame };
kResultOk
}
unsafe fn canResize(&self) -> tresult {
let delegate = unsafe { &*self.delegate.get() };
if delegate.gui_constraints().resizable { kResultOk } else { kResultFalse }
}
unsafe fn checkSizeConstraint(&self, rect: *mut ViewRect) -> tresult {
if rect.is_null() {
return kInvalidArgument;
}
let delegate = unsafe { &*self.delegate.get() };
let constraints = delegate.gui_constraints();
let r = unsafe { &mut *rect };
let mut width = (r.right - r.left).max(0) as u32;
let mut height = (r.bottom - r.top).max(0) as u32;
width = width.clamp(constraints.min.width, constraints.max.width);
height = height.clamp(constraints.min.height, constraints.max.height);
r.right = r.left + width as i32;
r.bottom = r.top + height as i32;
kResultOk
}
}
impl Drop for WebViewPlugView {
fn drop(&mut self) {
let ipc = self.ipc.get_mut();
#[cfg(target_os = "macos")]
{
if !ipc.sync_timer.is_null() {
unsafe {
let _: () = objc2::msg_send![ipc.sync_timer, invalidate];
}
ipc.sync_timer = std::ptr::null_mut();
}
}
ipc.webview = std::ptr::null();
unsafe { handler_release(ipc.handler) };
ipc.handler = std::ptr::null_mut();
let frame = *self.frame.get_mut();
if !frame.is_null() {
unsafe {
let unknown = frame as *mut FUnknown;
((*(*unknown).vtbl).release)(unknown);
}
}
}
}
pub struct StaticGuiDelegate {
size: Size,
constraints: GuiConstraints,
}
impl StaticGuiDelegate {
pub fn new(size: Size, constraints: GuiConstraints) -> Self {
Self { size, constraints }
}
}
impl GuiDelegate for StaticGuiDelegate {
fn gui_size(&self) -> Size {
self.size
}
fn gui_constraints(&self) -> GuiConstraints {
self.constraints
}
}
#[cfg(target_os = "macos")]
fn get_or_register_timer_class() -> &'static objc2::runtime::AnyClass {
use objc2::runtime::{AnyClass, ClassBuilder};
use objc2_foundation::NSObject;
use objc2::ClassType;
let c_name = c"BeamerSyncTimerTarget";
if let Some(existing) = AnyClass::get(c_name) {
return existing;
}
let superclass = NSObject::class();
let mut builder = match ClassBuilder::new(c_name, superclass) {
Some(b) => b,
None => {
return AnyClass::get(c_name)
.expect("class must exist after ClassBuilder::new returned None");
}
};
unsafe {
builder.add_method::<objc2::runtime::AnyObject, _>(
objc2::sel!(beamerSyncTimerFired:),
sync_timer_fired
as unsafe extern "C-unwind" fn(
*mut objc2::runtime::AnyObject,
objc2::runtime::Sel,
*mut objc2::runtime::AnyObject,
),
);
}
builder.register()
}