#[cfg(all(unix, not(target_os = "macos")))]
use crate::consts::plugins_lv2::{
GTK_WINDOW_TOPLEVEL, LV2_INSTANCE_ACCESS, LV2_UI_GTK, LV2_UI_GTK3, LV2_UI_HIDE_INTERFACE,
LV2_UI_IDLE_INTERFACE, LV2_UI_PARENT, LV2_UI_QT4, LV2_UI_QT5, LV2_UI_QT6, LV2_UI_RESIZE,
LV2_UI_SHOW_INTERFACE, LV2_UI_X11, LV2_URID_MAP, LV2_URID_MAP_TYPO_COMPAT, LV2_URID_UNMAP,
};
#[cfg(all(unix, not(target_os = "macos")))]
use lilv::{World, plugin::Plugin};
#[cfg(all(unix, not(target_os = "macos")))]
use maolan_engine::client::Client;
#[cfg(all(unix, not(target_os = "macos")))]
use maolan_engine::message::{Action, Lv2ControlPortInfo, Message as EngineMessage};
#[cfg(all(unix, not(target_os = "macos")))]
use std::collections::HashMap;
#[cfg(all(unix, not(target_os = "macos")))]
use std::ffi::{CStr, CString, c_char, c_uint, c_ulong, c_void};
#[cfg(all(unix, not(target_os = "macos")))]
use std::sync::{OnceLock, mpsc};
#[cfg(all(unix, not(target_os = "macos")))]
use std::thread;
#[cfg(all(unix, not(target_os = "macos")))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct WindowKey {
track_name: String,
clip_idx: Option<usize>,
instance_id: usize,
}
#[cfg(all(unix, not(target_os = "macos")))]
struct SliderCallbackData {
track_name: String,
clip_idx: Option<usize>,
instance_id: usize,
port_index: u32,
tx: mpsc::Sender<(String, Option<usize>, usize, u32, f32)>,
}
#[cfg(all(unix, not(target_os = "macos")))]
unsafe impl Send for SliderCallbackData {}
#[cfg(all(unix, not(target_os = "macos")))]
struct NativeUiController {
track_name: String,
clip_idx: Option<usize>,
instance_id: usize,
port_symbol_to_index: HashMap<String, u32>,
tx: mpsc::Sender<(String, Option<usize>, usize, u32, f32)>,
}
#[cfg(all(unix, not(target_os = "macos")))]
unsafe impl Send for NativeUiController {}
#[cfg(all(unix, not(target_os = "macos")))]
struct NativeWindowRuntime {
_window: *mut c_void,
suil_instance: *mut SuilInstance,
suil_host: *mut SuilHost,
controller_ptr: *mut NativeUiController,
idle_source: c_uint,
idle_data_ptr: *mut UiIdleData,
hide_data_ptr: *mut UiHideData,
show_iface_ptr: *const LV2UiShowInterface,
hide_iface_ptr: *const LV2UiHideInterface,
ui_handle: *mut c_void,
_urid_feature: UridMapFeature,
}
#[cfg(all(unix, not(target_os = "macos")))]
impl NativeWindowRuntime {
fn is_visible(&self) -> bool {
unsafe { gtk_widget_get_visible(self._window) != 0 }
}
fn show(&self) {
if !self.show_iface_ptr.is_null()
&& let Some(show) = unsafe { (*self.show_iface_ptr).show }
{
let _ = show(self.ui_handle);
}
unsafe {
gtk_widget_show_all(self._window);
gtk_window_present(self._window);
}
}
fn cleanup(self) {
if !self.hide_iface_ptr.is_null()
&& let Some(hide) = unsafe { (*self.hide_iface_ptr).hide }
{
let _ = hide(self.ui_handle);
}
if self.idle_source != 0 {
unsafe {
g_source_remove(self.idle_source);
}
}
if !self.idle_data_ptr.is_null() {
unsafe {
drop(Box::from_raw(self.idle_data_ptr));
}
}
if !self.hide_data_ptr.is_null() {
unsafe {
drop(Box::from_raw(self.hide_data_ptr));
}
}
unsafe {
suil_instance_free(self.suil_instance);
suil_host_free(self.suil_host);
drop(Box::from_raw(self.controller_ptr));
gtk_widget_destroy(self._window);
}
}
}
#[cfg(all(unix, not(target_os = "macos")))]
struct GenericWindowRuntime {
window: *mut c_void,
slider_data: Vec<*mut SliderCallbackData>,
}
#[cfg(all(unix, not(target_os = "macos")))]
impl GenericWindowRuntime {
fn is_visible(&self) -> bool {
unsafe { gtk_widget_get_visible(self.window) != 0 }
}
fn show(&self) {
unsafe {
gtk_widget_show_all(self.window);
gtk_window_present(self.window);
}
}
fn cleanup(self) {
for data_ptr in self.slider_data {
unsafe {
drop(Box::from_raw(data_ptr));
}
}
unsafe {
gtk_widget_destroy(self.window);
}
}
}
#[cfg(all(unix, not(target_os = "macos")))]
enum GtkThreadWindow {
Native(NativeWindowRuntime),
Generic(GenericWindowRuntime),
}
#[cfg(all(unix, not(target_os = "macos")))]
enum WindowEntry {
Native(NativeWindowRuntime),
Generic(GenericWindowRuntime),
}
#[cfg(all(unix, not(target_os = "macos")))]
impl WindowEntry {
fn is_visible(&self) -> bool {
match self {
Self::Native(window) => window.is_visible(),
Self::Generic(window) => window.is_visible(),
}
}
fn show(&self) {
match self {
Self::Native(window) => window.show(),
Self::Generic(window) => window.show(),
}
}
fn cleanup(self) {
match self {
Self::Native(window) => window.cleanup(),
Self::Generic(window) => window.cleanup(),
}
}
}
#[cfg(all(unix, not(target_os = "macos")))]
#[derive(Default)]
struct UridMapState {
next_urid: u32,
by_uri: HashMap<String, u32>,
by_urid: HashMap<u32, CString>,
}
#[cfg(all(unix, not(target_os = "macos")))]
#[repr(C)]
struct LV2FeatureRaw {
uri: *const c_char,
data: *mut c_void,
}
#[cfg(all(unix, not(target_os = "macos")))]
#[repr(C)]
struct LV2UridMap {
handle: *mut c_void,
map: extern "C" fn(handle: *mut c_void, uri: *const c_char) -> u32,
}
#[cfg(all(unix, not(target_os = "macos")))]
#[repr(C)]
struct LV2UridUnmap {
handle: *mut c_void,
unmap: extern "C" fn(handle: *mut c_void, urid: u32) -> *const c_char,
}
#[cfg(all(unix, not(target_os = "macos")))]
struct UridMapFeature {
_map_uri: CString,
_map_typo_uri: CString,
_unmap_uri: CString,
_map: Box<LV2UridMap>,
_unmap: Box<LV2UridUnmap>,
map_feature: LV2FeatureRaw,
map_typo_feature: LV2FeatureRaw,
unmap_feature: LV2FeatureRaw,
_state: Box<std::sync::Mutex<UridMapState>>,
}
#[cfg(all(unix, not(target_os = "macos")))]
unsafe impl Send for UridMapFeature {}
#[cfg(all(unix, not(target_os = "macos")))]
impl UridMapFeature {
fn new() -> Result<Self, String> {
let map_uri = CString::new(LV2_URID_MAP).map_err(|e| e.to_string())?;
let map_typo_uri = CString::new(LV2_URID_MAP_TYPO_COMPAT).map_err(|e| e.to_string())?;
let unmap_uri = CString::new(LV2_URID_UNMAP).map_err(|e| e.to_string())?;
let mut state = Box::new(std::sync::Mutex::new(UridMapState::default()));
let map = Box::new(LV2UridMap {
handle: (&mut *state as *mut std::sync::Mutex<UridMapState>).cast::<c_void>(),
map: lv2_urid_map_callback,
});
let unmap = Box::new(LV2UridUnmap {
handle: (&mut *state as *mut std::sync::Mutex<UridMapState>).cast::<c_void>(),
unmap: lv2_urid_unmap_callback,
});
let map_feature = LV2FeatureRaw {
uri: map_uri.as_ptr(),
data: (&*map as *const LV2UridMap).cast::<c_void>() as *mut c_void,
};
let map_typo_feature = LV2FeatureRaw {
uri: map_typo_uri.as_ptr(),
data: (&*map as *const LV2UridMap).cast::<c_void>() as *mut c_void,
};
let unmap_feature = LV2FeatureRaw {
uri: unmap_uri.as_ptr(),
data: (&*unmap as *const LV2UridUnmap).cast::<c_void>() as *mut c_void,
};
Ok(Self {
_map_uri: map_uri,
_map_typo_uri: map_typo_uri,
_unmap_uri: unmap_uri,
_map: map,
_unmap: unmap,
map_feature,
map_typo_feature,
unmap_feature,
_state: state,
})
}
}
#[cfg(all(unix, not(target_os = "macos")))]
extern "C" fn lv2_urid_map_callback(handle: *mut c_void, uri: *const c_char) -> u32 {
if handle.is_null() || uri.is_null() {
return 0;
}
let Some(uri_str) = (unsafe { CStr::from_ptr(uri) }).to_str().ok() else {
return 0;
};
let state = unsafe { &*(handle as *const std::sync::Mutex<UridMapState>) };
let Ok(mut state) = state.lock() else {
return 0;
};
if let Some(existing) = state.by_uri.get(uri_str) {
return *existing;
}
let next = state.next_urid.saturating_add(1).max(1);
state.next_urid = next;
let Ok(c_uri) = CString::new(uri_str) else {
return 0;
};
state.by_uri.insert(uri_str.to_string(), next);
state.by_urid.insert(next, c_uri);
next
}
#[cfg(all(unix, not(target_os = "macos")))]
extern "C" fn lv2_urid_unmap_callback(handle: *mut c_void, urid: u32) -> *const c_char {
if handle.is_null() || urid == 0 {
return std::ptr::null();
}
let state = unsafe { &*(handle as *const std::sync::Mutex<UridMapState>) };
let Ok(state) = state.lock() else {
return std::ptr::null();
};
state
.by_urid
.get(&urid)
.map(|s| s.as_ptr())
.unwrap_or(std::ptr::null())
}
#[cfg(all(unix, not(target_os = "macos")))]
#[derive(Debug, Clone)]
struct NativeUiSpec {
plugin_uri: String,
ui_uri: String,
container_type_uri: String,
ui_type_uri: String,
ui_bundle_path: String,
ui_binary_path: String,
port_symbol_to_index: HashMap<String, u32>,
}
#[cfg(all(unix, not(target_os = "macos")))]
pub(crate) struct Lv2UiTarget {
pub track_name: String,
pub clip_idx: Option<usize>,
pub instance_id: usize,
}
#[cfg(all(unix, not(target_os = "macos")))]
pub(crate) struct Lv2UiOpenRequest {
pub target: Lv2UiTarget,
pub plugin_name: String,
pub plugin_uri: String,
pub controls: Vec<Lv2ControlPortInfo>,
pub instance_access_handle: Option<usize>,
pub client: Client,
}
#[cfg(all(unix, not(target_os = "macos")))]
#[repr(C)]
struct LV2UiResize {
handle: *mut c_void,
ui_resize: Option<extern "C" fn(*mut c_void, i32, i32) -> i32>,
}
#[cfg(all(unix, not(target_os = "macos")))]
#[repr(C)]
struct LV2UiIdleInterface {
idle: Option<extern "C" fn(*mut c_void) -> i32>,
}
#[cfg(all(unix, not(target_os = "macos")))]
#[repr(C)]
struct LV2UiShowInterface {
show: Option<extern "C" fn(*mut c_void) -> i32>,
}
#[cfg(all(unix, not(target_os = "macos")))]
#[repr(C)]
struct LV2UiHideInterface {
hide: Option<extern "C" fn(*mut c_void) -> i32>,
}
#[cfg(all(unix, not(target_os = "macos")))]
struct UiIdleData {
interface: *const LV2UiIdleInterface,
handle: *mut c_void,
}
#[cfg(all(unix, not(target_os = "macos")))]
unsafe impl Send for UiIdleData {}
#[cfg(all(unix, not(target_os = "macos")))]
struct UiHideData {
interface: *const LV2UiHideInterface,
handle: *mut c_void,
}
#[cfg(all(unix, not(target_os = "macos")))]
extern "C" fn host_ui_resize(handle: *mut c_void, width: i32, height: i32) -> i32 {
if handle.is_null() || width <= 0 || height <= 0 {
return 1;
}
unsafe {
gtk_window_resize(handle, width, height);
}
0
}
#[cfg(all(unix, not(target_os = "macos")))]
extern "C" fn ui_idle_tick(data: *mut c_void) -> i32 {
if data.is_null() {
return 0;
}
let idle_data = unsafe { &*(data as *const UiIdleData) };
if idle_data.interface.is_null() {
return 0;
}
let interface = unsafe { &*idle_data.interface };
let Some(idle_fn) = interface.idle else {
return 0;
};
idle_fn(idle_data.handle)
}
#[cfg(all(unix, not(target_os = "macos")))]
#[allow(non_camel_case_types)]
type SuilHost = c_void;
#[cfg(all(unix, not(target_os = "macos")))]
#[allow(non_camel_case_types)]
type SuilInstance = c_void;
#[cfg(all(unix, not(target_os = "macos")))]
type SuilController = *mut c_void;
#[cfg(all(unix, not(target_os = "macos")))]
type SuilPortWriteFunc = extern "C" fn(SuilController, u32, u32, u32, *const c_void);
#[cfg(all(unix, not(target_os = "macos")))]
type SuilPortIndexFunc = extern "C" fn(SuilController, *const c_char) -> u32;
#[cfg(all(unix, not(target_os = "macos")))]
extern "C" fn suil_write_port(
controller: SuilController,
port_index: u32,
buffer_size: u32,
protocol: u32,
buffer: *const c_void,
) {
if controller.is_null() || buffer.is_null() || protocol != 0 || buffer_size != 4 {
return;
}
let controller = unsafe { &*(controller as *const NativeUiController) };
let value = unsafe { *(buffer as *const f32) };
if controller
.tx
.send((
controller.track_name.clone(),
controller.clip_idx,
controller.instance_id,
port_index,
value,
))
.is_err()
{
tracing::debug!("LV2 UI control channel closed while writing port");
}
}
#[cfg(all(unix, not(target_os = "macos")))]
extern "C" fn suil_port_index(controller: SuilController, port_symbol: *const c_char) -> u32 {
if controller.is_null() || port_symbol.is_null() {
return u32::MAX;
}
let controller = unsafe { &*(controller as *const NativeUiController) };
let Some(symbol) = unsafe { CStr::from_ptr(port_symbol) }.to_str().ok() else {
return u32::MAX;
};
controller
.port_symbol_to_index
.get(symbol)
.copied()
.unwrap_or(u32::MAX)
}
#[cfg(all(unix, not(target_os = "macos")))]
extern "C" fn suil_subscribe_port(
_controller: SuilController,
_port_index: u32,
_protocol: u32,
_features: *const *const LV2FeatureRaw,
) -> u32 {
0
}
#[cfg(all(unix, not(target_os = "macos")))]
extern "C" fn suil_unsubscribe_port(
_controller: SuilController,
_port_index: u32,
_protocol: u32,
_features: *const *const LV2FeatureRaw,
) -> u32 {
0
}
#[cfg(all(unix, not(target_os = "macos")))]
unsafe extern "C" fn on_slider_changed(range: *mut c_void, data: *mut c_void) {
if range.is_null() || data.is_null() {
return;
}
let data = unsafe { &*(data as *const SliderCallbackData) };
let value = unsafe { gtk_range_get_value(range) as f32 };
if data
.tx
.send((
data.track_name.clone(),
data.clip_idx,
data.instance_id,
data.port_index,
value,
))
.is_err()
{
tracing::debug!("LV2 slider callback channel closed");
}
}
#[cfg(all(unix, not(target_os = "macos")))]
unsafe extern "C" fn on_generic_slider_changed(range: *mut c_void, data: *mut c_void) {
unsafe { on_slider_changed(range, data) };
}
#[cfg(all(unix, not(target_os = "macos")))]
unsafe extern "C" fn on_gtk_delete(
widget: *mut c_void,
_event: *mut c_void,
data: *mut c_void,
) -> i32 {
if !data.is_null() {
let hide_data = unsafe { &*(data as *const UiHideData) };
if !hide_data.interface.is_null()
&& let Some(hide) = unsafe { (*hide_data.interface).hide }
{
let _ = hide(hide_data.handle);
}
}
unsafe {
gtk_widget_hide(widget);
}
1
}
#[cfg(all(unix, not(target_os = "macos")))]
pub struct GuiLv2UiHost {
windows: HashMap<WindowKey, WindowEntry>,
}
#[cfg(all(unix, not(target_os = "macos")))]
impl GuiLv2UiHost {
pub fn new() -> Self {
Self {
windows: HashMap::new(),
}
}
pub fn has_open_windows(&self) -> bool {
self.windows.values().any(WindowEntry::is_visible)
}
pub fn pump(&mut self) {
unsafe {
while gtk_events_pending() != 0 {
gtk_main_iteration_do(0);
}
}
}
pub fn open_editor(&mut self, request: Lv2UiOpenRequest) -> Result<(), String> {
ensure_gtk_initialized()?;
let key = WindowKey {
track_name: request.target.track_name.clone(),
clip_idx: request.target.clip_idx,
instance_id: request.target.instance_id,
};
if self.windows.contains_key(&key) {
if let Some(entry) = self.windows.get(&key) {
entry.show();
}
return Ok(());
}
let window = create_lv2_ui_window(request)?;
let entry = match window {
GtkThreadWindow::Native(window) => WindowEntry::Native(window),
GtkThreadWindow::Generic(window) => WindowEntry::Generic(window),
};
self.windows.insert(key, entry);
Ok(())
}
}
#[cfg(all(unix, not(target_os = "macos")))]
impl Drop for GuiLv2UiHost {
fn drop(&mut self) {
for (_, entry) in self.windows.drain() {
entry.cleanup();
}
}
}
#[cfg(all(unix, not(target_os = "macos")))]
fn ensure_gtk_initialized() -> Result<(), String> {
static GTK_INIT_RESULT: OnceLock<Result<(), String>> = OnceLock::new();
GTK_INIT_RESULT
.get_or_init(|| {
let mut argc = 0;
let mut argv: *mut *mut c_char = std::ptr::null_mut();
if unsafe { gtk_init_check(&mut argc, &mut argv) } == 0 {
Err("Failed to initialize GTK for LV2 UI host".to_string())
} else {
Ok(())
}
})
.clone()
}
#[cfg(all(unix, not(target_os = "macos")))]
fn create_lv2_ui_window(request: Lv2UiOpenRequest) -> Result<GtkThreadWindow, String> {
let tx = spawn_control_sender(request.client);
let ui_spec = resolve_preferred_ui(&request.plugin_uri);
if let Ok(spec) = ui_spec {
create_native_ui_window(
request.target,
request.plugin_name,
spec,
request.controls,
request.instance_access_handle,
&tx,
)
} else {
create_generic_ui_window(request.target, request.plugin_name, request.controls, &tx)
}
}
fn create_native_ui_window(
target: Lv2UiTarget,
plugin_name: String,
ui_spec: NativeUiSpec,
controls: Vec<Lv2ControlPortInfo>,
instance_access_handle: Option<usize>,
tx: &mpsc::Sender<(String, Option<usize>, usize, u32, f32)>,
) -> Result<GtkThreadWindow, String> {
let window = unsafe { gtk_window_new(GTK_WINDOW_TOPLEVEL) };
if window.is_null() {
return Err("Failed to create LV2 UI window".to_string());
}
let title = CString::new(format!("LV2 UI - {}", plugin_name)).map_err(|e| e.to_string())?;
unsafe {
gtk_window_set_title(window, title.as_ptr());
gtk_window_set_default_size(window, 780, 520);
gtk_window_set_type_hint(window, 1);
}
let use_x11_parent = ui_spec.container_type_uri == LV2_UI_X11;
let parent_widget = if use_x11_parent {
let socket = unsafe { gtk_socket_new() };
if socket.is_null() {
unsafe { gtk_widget_destroy(window) };
return Err("Failed to create GtkSocket for X11 UI embedding".to_string());
}
unsafe {
gtk_container_add(window, socket);
gtk_widget_show_all(window);
gtk_widget_realize(socket);
}
let xid = unsafe { gtk_socket_get_id(socket) };
xid as *mut c_void
} else {
let container = unsafe { gtk_alignment_new(0.0, 0.0, 1.0, 1.0) };
if container.is_null() {
unsafe { gtk_widget_destroy(window) };
return Err("Failed to create GTK container for LV2 UI embedding".to_string());
}
unsafe {
gtk_container_add(window, container);
gtk_widget_show_all(window);
}
container
};
let controller = Box::new(NativeUiController {
track_name: target.track_name,
clip_idx: target.clip_idx,
instance_id: target.instance_id,
port_symbol_to_index: ui_spec.port_symbol_to_index,
tx: tx.clone(),
});
let controller_ptr = Box::into_raw(controller);
let suil_host = unsafe {
suil_host_new(
Some(suil_write_port),
Some(suil_port_index),
Some(suil_subscribe_port),
Some(suil_unsubscribe_port),
)
};
if suil_host.is_null() {
unsafe {
drop(Box::from_raw(controller_ptr));
gtk_widget_destroy(window);
}
return Err("Failed to create suil host".to_string());
}
let container_type_uri = CString::new(ui_spec.container_type_uri).map_err(|e| e.to_string())?;
let plugin_uri = CString::new(ui_spec.plugin_uri).map_err(|e| e.to_string())?;
let ui_uri = CString::new(ui_spec.ui_uri).map_err(|e| e.to_string())?;
let ui_type_uri = CString::new(ui_spec.ui_type_uri).map_err(|e| e.to_string())?;
let ui_bundle_path = CString::new(ui_spec.ui_bundle_path).map_err(|e| e.to_string())?;
let ui_binary_path = CString::new(ui_spec.ui_binary_path).map_err(|e| e.to_string())?;
let urid_feature = UridMapFeature::new().inspect_err(|_e| unsafe {
suil_host_free(suil_host);
drop(Box::from_raw(controller_ptr));
gtk_widget_destroy(window);
})?;
let parent_uri = CString::new(LV2_UI_PARENT).map_err(|e| e.to_string())?;
let resize_uri = CString::new(LV2_UI_RESIZE).map_err(|e| e.to_string())?;
let instance_access_uri = CString::new(LV2_INSTANCE_ACCESS).map_err(|e| e.to_string())?;
let mut resize_feature = LV2UiResize {
handle: window,
ui_resize: Some(host_ui_resize),
};
let parent_raw = LV2FeatureRaw {
uri: parent_uri.as_ptr(),
data: parent_widget,
};
let resize_raw = LV2FeatureRaw {
uri: resize_uri.as_ptr(),
data: (&mut resize_feature as *mut LV2UiResize).cast::<c_void>(),
};
let instance_access_raw = instance_access_handle.map(|handle| LV2FeatureRaw {
uri: instance_access_uri.as_ptr(),
data: handle as *mut c_void,
});
let mut feature_ptrs: Vec<*const LV2FeatureRaw> = vec![
&urid_feature.map_feature,
&urid_feature.map_typo_feature,
&urid_feature.unmap_feature,
&parent_raw,
&resize_raw,
];
if let Some(ref raw) = instance_access_raw {
feature_ptrs.push(raw as *const LV2FeatureRaw);
}
feature_ptrs.push(std::ptr::null());
let suil_instance = unsafe {
suil_instance_new(
suil_host,
controller_ptr.cast::<c_void>(),
container_type_uri.as_ptr(),
plugin_uri.as_ptr(),
ui_uri.as_ptr(),
ui_type_uri.as_ptr(),
ui_bundle_path.as_ptr(),
ui_binary_path.as_ptr(),
feature_ptrs.as_ptr(),
)
};
if suil_instance.is_null() {
unsafe {
suil_host_free(suil_host);
drop(Box::from_raw(controller_ptr));
gtk_widget_destroy(window);
}
return Err("Failed to instantiate suil UI".to_string());
}
let widget = unsafe { suil_instance_get_widget(suil_instance) };
if widget.is_null() {
unsafe {
suil_instance_free(suil_instance);
suil_host_free(suil_host);
drop(Box::from_raw(controller_ptr));
gtk_widget_destroy(window);
}
return Err("Suil returned null UI widget".to_string());
}
let ui_handle = unsafe { suil_instance_get_handle(suil_instance) };
let idle_iface_uri = CString::new(LV2_UI_IDLE_INTERFACE).map_err(|e| e.to_string())?;
let show_iface_uri = CString::new(LV2_UI_SHOW_INTERFACE).map_err(|e| e.to_string())?;
let hide_iface_uri = CString::new(LV2_UI_HIDE_INTERFACE).map_err(|e| e.to_string())?;
let idle_iface_ptr = unsafe {
suil_instance_extension_data(suil_instance, idle_iface_uri.as_ptr())
as *const LV2UiIdleInterface
};
let show_iface_ptr = unsafe {
suil_instance_extension_data(suil_instance, show_iface_uri.as_ptr())
as *const LV2UiShowInterface
};
let hide_iface_ptr = unsafe {
suil_instance_extension_data(suil_instance, hide_iface_uri.as_ptr())
as *const LV2UiHideInterface
};
let hide_data_ptr = Box::into_raw(Box::new(UiHideData {
interface: hide_iface_ptr,
handle: ui_handle,
}));
unsafe {
g_signal_connect_data(
window,
c"delete-event".as_ptr(),
on_gtk_delete as *const c_void,
hide_data_ptr.cast::<c_void>(),
None,
0,
);
}
let mut idle_source: c_uint = 0;
let mut idle_data_ptr: *mut UiIdleData = std::ptr::null_mut();
if !idle_iface_ptr.is_null() {
let idle_data = Box::new(UiIdleData {
interface: idle_iface_ptr,
handle: ui_handle,
});
idle_data_ptr = Box::into_raw(idle_data);
unsafe {
idle_source = g_timeout_add(16, Some(ui_idle_tick), idle_data_ptr.cast::<c_void>());
}
}
if !show_iface_ptr.is_null()
&& let Some(show) = unsafe { (*show_iface_ptr).show }
{
let _ = show(ui_handle);
}
for port in controls {
let value = port.value;
unsafe {
suil_instance_port_event(
suil_instance,
port.index,
std::mem::size_of::<f32>() as u32,
0,
(&value as *const f32).cast::<c_void>(),
);
}
}
unsafe {
if !use_x11_parent {
if gtk_bin_get_child(parent_widget).is_null() {
gtk_container_add(parent_widget, widget);
}
gtk_widget_show_all(window);
}
}
Ok(GtkThreadWindow::Native(NativeWindowRuntime {
_window: window,
suil_instance,
suil_host,
controller_ptr,
idle_source,
idle_data_ptr,
hide_data_ptr,
show_iface_ptr,
hide_iface_ptr,
ui_handle,
_urid_feature: urid_feature,
}))
}
fn create_generic_ui_window(
target: Lv2UiTarget,
plugin_name: String,
controls: Vec<Lv2ControlPortInfo>,
tx: &mpsc::Sender<(String, Option<usize>, usize, u32, f32)>,
) -> Result<GtkThreadWindow, String> {
let window = unsafe { gtk_window_new(GTK_WINDOW_TOPLEVEL) };
if window.is_null() {
return Err("Failed to create generic parameter UI window".to_string());
}
let title =
CString::new(format!("LV2 Generic UI - {}", plugin_name)).map_err(|e| e.to_string())?;
unsafe {
gtk_window_set_title(window, title.as_ptr());
gtk_window_set_default_size(window, 720, 480);
gtk_window_set_type_hint(window, 1);
g_signal_connect_data(
window,
c"delete-event".as_ptr(),
on_gtk_delete as *const c_void,
std::ptr::null_mut(),
None,
0,
);
}
let root = unsafe { gtk_vbox_new(0, 8) };
if root.is_null() {
unsafe { gtk_widget_destroy(window) };
return Err("Failed to create generic parameter UI root".to_string());
}
let mut slider_data = vec![];
for port in controls {
let row = unsafe { gtk_hbox_new(0, 8) };
if row.is_null() {
continue;
}
let label_txt = CString::new(port.name).map_err(|e| e.to_string())?;
let label = unsafe { gtk_label_new(label_txt.as_ptr()) };
let step = ((port.max - port.min).abs() / 200.0).max(0.0001) as f64;
let slider = unsafe { gtk_hscale_new_with_range(port.min as f64, port.max as f64, step) };
if slider.is_null() {
continue;
}
unsafe {
gtk_widget_set_size_request(slider, 420, -1);
gtk_range_set_value(slider, port.value as f64);
}
let data = Box::new(SliderCallbackData {
track_name: target.track_name.clone(),
clip_idx: target.clip_idx,
instance_id: target.instance_id,
port_index: port.index,
tx: tx.clone(),
});
let data_ptr = Box::into_raw(data);
slider_data.push(data_ptr);
unsafe {
g_signal_connect_data(
slider,
c"value-changed".as_ptr(),
on_generic_slider_changed as *const c_void,
data_ptr.cast::<c_void>(),
None,
0,
);
gtk_box_pack_start(row, label, 0, 0, 4);
gtk_box_pack_start(row, slider, 1, 1, 4);
gtk_box_pack_start(root, row, 0, 0, 2);
}
}
unsafe {
gtk_container_add(window, root);
gtk_widget_show_all(window);
}
Ok(GtkThreadWindow::Generic(GenericWindowRuntime {
window,
slider_data,
}))
}
fn spawn_control_sender(client: Client) -> mpsc::Sender<(String, Option<usize>, usize, u32, f32)> {
let (tx, rx) = mpsc::channel::<(String, Option<usize>, usize, u32, f32)>();
thread::spawn(move || {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(_) => return,
};
for (track_name, clip_idx, instance_id, index, value) in rx {
let action = if let Some(clip_idx) = clip_idx {
Action::ClipSetLv2ControlValue {
track_name,
clip_idx,
instance_id,
index,
value,
}
} else {
Action::TrackSetLv2ControlValue {
track_name,
instance_id,
index,
value,
}
};
let _ = runtime.block_on(client.send(EngineMessage::Request(action)));
}
});
tx
}
#[cfg(all(unix, not(target_os = "macos")))]
fn port_symbol_map(plugin: &Plugin) -> HashMap<String, u32> {
let mut map = HashMap::new();
for port in plugin.iter_ports() {
if let Some(symbol) = port.symbol().and_then(|n| n.as_str().map(str::to_string)) {
map.insert(symbol, port.index() as u32);
}
}
map
}
#[cfg(all(unix, not(target_os = "macos")))]
fn resolve_preferred_ui(plugin_uri: &str) -> Result<NativeUiSpec, String> {
let world = World::new();
world.load_all();
let uri_node = world.new_uri(plugin_uri);
let plugin = world
.plugins()
.plugin(&uri_node)
.ok_or_else(|| format!("Plugin not found for URI: {plugin_uri}"))?;
let port_symbol_to_index = port_symbol_map(&plugin);
let uis = plugin
.uis()
.ok_or_else(|| format!("Plugin has no UI: {plugin_uri}"))?;
let gtk3_uri = world.new_uri(LV2_UI_GTK3);
let gtk_uri = world.new_uri(LV2_UI_GTK);
let x11_uri = world.new_uri(LV2_UI_X11);
let qt4_uri = world.new_uri(LV2_UI_QT4);
let qt5_uri = world.new_uri(LV2_UI_QT5);
let qt6_uri = world.new_uri(LV2_UI_QT6);
let ui_classes = [
(>k3_uri, LV2_UI_GTK3),
(>k_uri, LV2_UI_GTK),
(&x11_uri, LV2_UI_X11),
(&qt6_uri, LV2_UI_QT6),
(&qt5_uri, LV2_UI_QT5),
(&qt4_uri, LV2_UI_QT4),
];
let allow_x11_host_container = matches!(
std::env::var("MAOLAN_LV2_ALLOW_X11_UI").ok().as_deref(),
Some("1") | Some("true") | Some("TRUE") | Some("True")
);
let mut best: Option<(usize, usize, u32, NativeUiSpec)> = None;
for ui in uis.iter() {
let ui_uri = ui
.uri()
.as_uri()
.ok_or_else(|| "UI URI is invalid".to_string())?
.to_string();
let bundle_uri = ui
.bundle_uri()
.ok_or_else(|| "UI bundle URI missing".to_string())?;
let binary_uri = ui
.binary_uri()
.ok_or_else(|| "UI binary URI missing".to_string())?;
let (_, ui_bundle_path) = bundle_uri
.path()
.ok_or_else(|| "Failed to resolve UI bundle path".to_string())?;
let (_, ui_binary_path) = binary_uri
.path()
.ok_or_else(|| "Failed to resolve UI binary path".to_string())?;
for (class_rank, (class_node, class_uri)) in ui_classes.iter().enumerate() {
if !ui.is_a(class_node) {
continue;
}
let class_c = CString::new(*class_uri).map_err(|e| e.to_string())?;
let mut host_containers: Vec<&str> = vec![LV2_UI_GTK];
if allow_x11_host_container {
host_containers.push(LV2_UI_X11);
}
for container_uri in host_containers {
let host_type = CString::new(container_uri).map_err(|e| e.to_string())?;
let quality = unsafe { suil_ui_supported(host_type.as_ptr(), class_c.as_ptr()) };
if quality == 0 {
continue;
}
let container_rank = usize::from(container_uri != LV2_UI_GTK);
let spec = NativeUiSpec {
plugin_uri: plugin_uri.to_string(),
ui_uri: ui_uri.clone(),
container_type_uri: container_uri.to_string(),
ui_type_uri: (*class_uri).to_string(),
ui_bundle_path: ui_bundle_path.clone(),
ui_binary_path: ui_binary_path.clone(),
port_symbol_to_index: port_symbol_to_index.clone(),
};
let is_better = match &best {
None => true,
Some((best_class_rank, best_container_rank, best_quality, _)) => {
class_rank < *best_class_rank
|| (class_rank == *best_class_rank
&& (container_rank < *best_container_rank
|| (container_rank == *best_container_rank
&& quality > *best_quality)))
}
};
if is_better {
best = Some((class_rank, container_rank, quality, spec));
}
}
}
}
best.map(|(_, _, _, spec)| spec)
.ok_or_else(|| format!("No supported native UI found for plugin: {plugin_uri}"))
}
#[cfg(all(unix, not(target_os = "macos")))]
#[link(name = "suil-0")]
unsafe extern "C" {
fn suil_host_new(
write_func: Option<SuilPortWriteFunc>,
index_func: Option<SuilPortIndexFunc>,
subscribe_func: Option<
extern "C" fn(SuilController, u32, u32, *const *const LV2FeatureRaw) -> u32,
>,
unsubscribe_func: Option<
extern "C" fn(SuilController, u32, u32, *const *const LV2FeatureRaw) -> u32,
>,
) -> *mut SuilHost;
fn suil_host_free(host: *mut SuilHost);
fn suil_ui_supported(host_type_uri: *const c_char, ui_type_uri: *const c_char) -> u32;
fn suil_instance_new(
host: *mut SuilHost,
controller: SuilController,
container_type_uri: *const c_char,
plugin_uri: *const c_char,
ui_uri: *const c_char,
ui_type_uri: *const c_char,
ui_bundle_path: *const c_char,
ui_binary_path: *const c_char,
features: *const *const LV2FeatureRaw,
) -> *mut SuilInstance;
fn suil_instance_free(instance: *mut SuilInstance);
fn suil_instance_get_widget(instance: *mut SuilInstance) -> *mut c_void;
fn suil_instance_get_handle(instance: *mut SuilInstance) -> *mut c_void;
fn suil_instance_extension_data(
instance: *mut SuilInstance,
uri: *const c_char,
) -> *const c_void;
fn suil_instance_port_event(
instance: *mut SuilInstance,
port_index: u32,
buffer_size: u32,
protocol: u32,
buffer: *const c_void,
);
}
#[cfg(all(unix, not(target_os = "macos")))]
#[link(name = "gtk-x11-2.0")]
unsafe extern "C" {
fn gtk_init_check(argc: *mut i32, argv: *mut *mut *mut c_char) -> i32;
fn gtk_window_new(window_type: i32) -> *mut c_void;
fn gtk_window_set_title(window: *mut c_void, title: *const c_char);
fn gtk_window_set_default_size(window: *mut c_void, width: i32, height: i32);
fn gtk_window_set_type_hint(window: *mut c_void, hint: i32);
fn gtk_window_present(window: *mut c_void);
fn gtk_window_resize(window: *mut c_void, width: i32, height: i32);
fn gtk_alignment_new(xalign: f32, yalign: f32, xscale: f32, yscale: f32) -> *mut c_void;
fn gtk_bin_get_child(bin: *mut c_void) -> *mut c_void;
fn gtk_container_add(container: *mut c_void, widget: *mut c_void);
fn gtk_vbox_new(homogeneous: i32, spacing: i32) -> *mut c_void;
fn gtk_hbox_new(homogeneous: i32, spacing: i32) -> *mut c_void;
fn gtk_label_new(text: *const c_char) -> *mut c_void;
fn gtk_hscale_new_with_range(min: f64, max: f64, step: f64) -> *mut c_void;
fn gtk_range_set_value(range: *mut c_void, value: f64);
fn gtk_range_get_value(range: *mut c_void) -> f64;
fn gtk_widget_set_size_request(widget: *mut c_void, width: i32, height: i32);
fn gtk_box_pack_start(
boxed: *mut c_void,
child: *mut c_void,
expand: i32,
fill: i32,
padding: u32,
);
fn gtk_widget_destroy(widget: *mut c_void);
fn gtk_widget_hide(widget: *mut c_void);
fn gtk_widget_get_visible(widget: *mut c_void) -> i32;
fn gtk_widget_show_all(widget: *mut c_void);
fn gtk_widget_realize(widget: *mut c_void);
fn gtk_socket_new() -> *mut c_void;
fn gtk_socket_get_id(socket: *mut c_void) -> c_ulong;
fn gtk_events_pending() -> i32;
fn gtk_main_iteration_do(blocking: i32);
}
#[cfg(all(unix, not(target_os = "macos")))]
#[link(name = "gobject-2.0")]
unsafe extern "C" {
fn g_signal_connect_data(
instance: *mut c_void,
detailed_signal: *const c_char,
c_handler: *const c_void,
data: *mut c_void,
destroy_data: Option<unsafe extern "C" fn(*mut c_void, *mut c_void)>,
connect_flags: u32,
) -> u64;
}
#[cfg(all(unix, not(target_os = "macos")))]
#[link(name = "glib-2.0")]
unsafe extern "C" {
fn g_timeout_add(
interval: c_uint,
function: Option<extern "C" fn(*mut c_void) -> i32>,
data: *mut c_void,
) -> c_uint;
fn g_source_remove(tag: c_uint) -> i32;
}