diff --git a/examples/gain/Cargo.toml b/examples/gain/Cargo.toml index 73ddf19a..ee9bd5a3 100644 --- a/examples/gain/Cargo.toml +++ b/examples/gain/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] coupler = { workspace = true, features = ["derive"] } -portlight = { git = "https://github.com/coupler-rs/portlight", rev = "fff171e9f3f70a9604102e1e1877ad67d0c2badd" } +portlight = { git = "https://github.com/coupler-rs/portlight", rev = "e517f1e7e1700b70a74523cf905d5c80f81b4e81" } flicker = { git = "https://github.com/coupler-rs/flicker", rev = "80aca05cb6c7406f8c2a4ba66d85849dc344afaa" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/examples/gain/src/lib.rs b/examples/gain/src/lib.rs index f5b2b83b..a6b6f1f8 100644 --- a/examples/gain/src/lib.rs +++ b/examples/gain/src/lib.rs @@ -1,8 +1,7 @@ -use std::cell::RefCell; use std::fmt::{self, Formatter}; use std::io::{self, Read, Write}; -use std::rc::Rc; - +use std::os::fd::AsRawFd; +use std::os::raw::c_int; use serde::{Deserialize, Serialize}; use coupler::format::clap::*; @@ -14,8 +13,8 @@ use coupler::{buffers::*, bus::*, engine::*, events::*, host::*, params::*, plug use flicker::Renderer; use portlight::{ - App, AppMode, AppOptions, Bitmap, Cursor, MouseButton, Point, RawWindow, Response, Window, - WindowContext, WindowOptions, + Bitmap, Context, Cursor, EventLoop, EventLoopMode, EventLoopOptions, Key, MouseButton, Point, + RawWindow, Response, Task, TaskHandle, Window, WindowEvent, WindowOptions, }; #[derive(Params, Serialize, Deserialize, Clone)] @@ -172,7 +171,8 @@ struct Gesture { struct ViewState { host: ViewHost, - params: Rc>, + params: GainParams, + window: Option, renderer: Renderer, framebuffer: Vec, mouse_pos: Point, @@ -180,10 +180,11 @@ struct ViewState { } impl ViewState { - fn new(host: ViewHost, params: Rc>) -> ViewState { + fn new(host: ViewHost, params: &GainParams) -> ViewState { ViewState { host, - params, + params: params.clone(), + window: None, renderer: Renderer::new(), framebuffer: Vec::new(), mouse_pos: Point { x: -1.0, y: -1.0 }, @@ -199,92 +200,96 @@ impl ViewState { window.set_cursor(Cursor::Arrow); } } +} - fn handle_event(&mut self, cx: &WindowContext, event: portlight::Event) -> Response { +impl Task for ViewState { + fn event(&mut self, _cx: &Context, _key: Key, event: portlight::Event) -> Response { use flicker::{Affine, Color, Path, Point}; use portlight::Event; - match event { - Event::Frame => { - let scale = cx.window().scale(); - let size = cx.window().size(); - let width = (size.width * scale) as usize; - let height = (size.height * scale) as usize; - self.framebuffer.resize(width * height, 0xFF000000); - - let mut target = self.renderer.attach(&mut self.framebuffer, width, height); - - target.clear(Color::rgba(21, 26, 31, 255)); - - let transform = Affine::scale(scale as f32); - - let value = self.params.borrow().gain; - - let center = Point::new(128.0, 128.0); - let radius = 32.0; - let angle1 = 0.75 * std::f32::consts::PI; - let angle2 = angle1 + value * 1.5 * std::f32::consts::PI; - let mut path = Path::new(); - path.move_to(center + radius * Point::new(angle1.cos(), angle1.sin())); - path.arc(radius, angle1, angle2); - path.line_to(center + (radius - 4.0) * Point::new(angle2.cos(), angle2.sin())); - path.arc(radius - 4.0, angle2, angle1); - path.close(); - target.fill_path(&path, transform, Color::rgba(240, 240, 245, 255)); - - let center = Point::new(128.0, 128.0); - let radius = 32.0; - let angle = 0.75 * std::f32::consts::PI; - let span = 1.5 * std::f32::consts::PI; - let mut path = Path::new(); - path.move_to(center + radius * Point::new(angle.cos(), angle.sin())); - path.arc(radius, angle, angle + span); - path.line_to(center + (radius - 4.0) * Point::new(-angle.cos(), angle.sin())); - path.arc(radius - 4.0, angle + span, angle); - path.close(); - target.stroke_path(&path, 1.0, transform, Color::rgba(240, 240, 245, 255)); - - cx.window().present(Bitmap::new(&self.framebuffer, width, height)); - } - Event::MouseMove(pos) => { - self.mouse_pos = pos; - if let Some(gesture) = &self.gesture { - let delta = -0.005 * (pos.y - gesture.start_mouse_pos.y) as f32; - let new_value = (gesture.start_value + delta).clamp(0.0, 1.0); - self.host.set_param(0, new_value as f64); - self.params.borrow_mut().gain = new_value; - } else { - self.update_cursor(cx.window()); + if let (Some(window), Event::Window(event)) = (&self.window, event) { + match event { + WindowEvent::Frame => { + let scale = window.scale(); + let size = window.size(); + let width = (size.width * scale) as usize; + let height = (size.height * scale) as usize; + self.framebuffer.resize(width * height, 0xFF000000); + + let mut target = self.renderer.attach(&mut self.framebuffer, width, height); + + target.clear(Color::rgba(21, 26, 31, 255)); + + let transform = Affine::scale(scale as f32); + + let value = self.params.gain; + + let center = Point::new(128.0, 128.0); + let radius = 32.0; + let angle1 = 0.75 * std::f32::consts::PI; + let angle2 = angle1 + value * 1.5 * std::f32::consts::PI; + let mut path = Path::new(); + path.move_to(center + radius * Point::new(angle1.cos(), angle1.sin())); + path.arc(radius, angle1, angle2); + path.line_to(center + (radius - 4.0) * Point::new(angle2.cos(), angle2.sin())); + path.arc(radius - 4.0, angle2, angle1); + path.close(); + target.fill_path(&path, transform, Color::rgba(240, 240, 245, 255)); + + let center = Point::new(128.0, 128.0); + let radius = 32.0; + let angle = 0.75 * std::f32::consts::PI; + let span = 1.5 * std::f32::consts::PI; + let mut path = Path::new(); + path.move_to(center + radius * Point::new(angle.cos(), angle.sin())); + path.arc(radius, angle, angle + span); + path.line_to(center + (radius - 4.0) * Point::new(-angle.cos(), angle.sin())); + path.arc(radius - 4.0, angle + span, angle); + path.close(); + target.stroke_path(&path, 1.0, transform, Color::rgba(240, 240, 245, 255)); + + window.present(Bitmap::new(&self.framebuffer, width, height)); } - } - Event::MouseDown(button) => { - if button == MouseButton::Left { - let pos = self.mouse_pos; - if pos.x >= 96.0 && pos.x < 160.0 && pos.y >= 96.0 && pos.y < 160.0 { - cx.window().set_cursor(Cursor::SizeNs); - self.host.begin_gesture(0); - let value = self.params.borrow().gain; - self.host.set_param(0, value as f64); - self.params.borrow_mut().gain = value; - self.gesture = Some(Gesture { - start_mouse_pos: pos, - start_value: value, - }); - return Response::Capture; + WindowEvent::MouseMove(pos) => { + self.mouse_pos = pos; + if let Some(gesture) = &self.gesture { + let delta = -0.005 * (pos.y - gesture.start_mouse_pos.y) as f32; + let new_value = (gesture.start_value + delta).clamp(0.0, 1.0); + self.host.set_param(0, new_value as f64); + self.params.gain = new_value; + } else { + self.update_cursor(window); } } - } - Event::MouseUp(button) => { - if button == MouseButton::Left { - if self.gesture.is_some() { - self.host.end_gesture(0); - self.gesture = None; - self.update_cursor(cx.window()); - return Response::Capture; + WindowEvent::MouseDown(button) => { + if button == MouseButton::Left { + let pos = self.mouse_pos; + if pos.x >= 96.0 && pos.x < 160.0 && pos.y >= 96.0 && pos.y < 160.0 { + window.set_cursor(Cursor::SizeNs); + self.host.begin_gesture(0); + let value = self.params.gain; + self.host.set_param(0, value as f64); + self.params.gain = value; + self.gesture = Some(Gesture { + start_mouse_pos: pos, + start_value: value, + }); + return Response::Capture; + } + } + } + WindowEvent::MouseUp(button) => { + if button == MouseButton::Left { + if self.gesture.is_some() { + self.host.end_gesture(0); + self.gesture = None; + self.update_cursor(window); + return Response::Capture; + } } } + _ => {} } - _ => {} } Response::Ignore @@ -293,9 +298,8 @@ impl ViewState { pub struct GainView { #[allow(unused)] - app: App, - window: Window, - params: Rc>, + event_loop: EventLoop, + task: TaskHandle, } impl GainView { @@ -304,35 +308,36 @@ impl GainView { parent: &ParentWindow, params: &GainParams, ) -> portlight::Result { - let app = AppOptions::new().mode(AppMode::Guest).build()?; + let event_loop = EventLoopOptions::new().mode(EventLoopMode::Guest).build()?; + + let task = event_loop.spawn(ViewState::new(host, params)); - let mut options = WindowOptions::new(); - options.size(portlight::Size::new(256.0, 256.0)); + task.with(|state, cx| { + let mut options = WindowOptions::new(); + options.size(portlight::Size::new(256.0, 256.0)); - let raw_parent = match parent.as_raw() { - RawParent::Win32(window) => RawWindow::Win32(window), - RawParent::Cocoa(view) => RawWindow::Cocoa(view), - RawParent::X11(window) => RawWindow::X11(window), - }; - unsafe { options.raw_parent(raw_parent) }; + let raw_parent = match parent.as_raw() { + RawParent::Win32(window) => RawWindow::Win32(window), + RawParent::Cocoa(view) => RawWindow::AppKit(view), + RawParent::X11(window) => RawWindow::X11(window), + }; + unsafe { options.raw_parent(raw_parent) }; - let params = Rc::new(RefCell::new(params.clone())); - let mut state = ViewState::new(host, Rc::clone(¶ms)); - let window = options.open(app.handle(), move |cx, event| state.handle_event(cx, event))?; + let window = options.open(cx, Key(0))?; + window.show(); - window.show(); + state.window = Some(window); - Ok(GainView { - app, - window, - params, - }) + portlight::Result::Ok(()) + })?; + + Ok(GainView { event_loop, task }) } } impl View for GainView { fn size(&self) -> Size { - let size = self.window.size(); + let size = self.task.with(|state, _| state.window.as_ref().unwrap().size()); Size { width: size.width, @@ -341,6 +346,16 @@ impl View for GainView { } fn param_changed(&mut self, id: ParamId, value: ParamValue) { - self.params.borrow_mut().set_param(id, value); + self.task.with(|state, _| { + state.params.set_param(id, value); + }); + } + + fn file_descriptor(&self) -> Option { + Some(self.event_loop.as_raw_fd()) + } + + fn poll(&mut self) { + self.event_loop.poll().unwrap(); } } diff --git a/src/format/vst3/component.rs b/src/format/vst3/component.rs index 139d7863..953a8d68 100644 --- a/src/format/vst3/component.rs +++ b/src/format/vst3/component.rs @@ -716,7 +716,7 @@ impl IEditControllerTrait for Component

{ return ptr::null_mut(); } - let view = ComWrapper::new(PlugView::new(&self.main_thread_state)); - view.to_com_ptr::().unwrap().into_raw() + let view = PlugView::new(&self.main_thread_state); + view.into_raw() } } diff --git a/src/format/vst3/view.rs b/src/format/vst3/view.rs index b4da8f09..70c77c3c 100644 --- a/src/format/vst3/view.rs +++ b/src/format/vst3/view.rs @@ -1,23 +1,25 @@ -use std::cell::{RefCell, UnsafeCell}; +use std::cell::{Cell, RefCell, UnsafeCell}; use std::ffi::{c_void, CStr}; use std::sync::Arc; -use vst3::Steinberg::Vst::{IComponentHandler, IComponentHandlerTrait}; -use vst3::{Class, ComPtr, Steinberg::*}; - use super::component::MainThreadState; use crate::params::{ParamId, ParamValue}; use crate::plugin::Plugin; use crate::view::{ParentWindow, RawParent, View, ViewHost, ViewHostInner}; +use vst3::Steinberg::Linux::{FileDescriptor, IEventHandlerTrait}; +use vst3::Steinberg::Vst::{IComponentHandler, IComponentHandlerTrait}; +use vst3::{Class, Interface, ComPtr, ComRef, ComWrapper, Steinberg::*}; pub struct Vst3ViewHost { pub handler: RefCell>>, + pub plug_frame: RefCell>>, } impl Vst3ViewHost { pub fn new() -> Vst3ViewHost { Vst3ViewHost { handler: RefCell::new(None), + plug_frame: RefCell::new(None), } } } @@ -53,18 +55,49 @@ impl ViewHostInner for Vst3ViewHost { pub struct PlugView { main_thread_state: Arc>>, + /// Raw pointer allows accessing this PlugView as a COM object when needed. + /// We use this rather than a ComPtr to avoid a circular reference / memory leak. + /// It needs to be a cell so we can have interior mutability and actually populate this + /// field after construction. This will allow us to later turn the IPlugView into + /// IEventHandler / ITimerHandler. + self_ptr: Cell>, } -impl PlugView

{ - pub fn new(main_thread_state: &Arc>>) -> PlugView

{ - PlugView { - main_thread_state: main_thread_state.clone(), +#[cfg(target_os = "linux")] +mod linux { + use super::*; + use vst3::Steinberg::Linux::*; + + impl IEventHandlerTrait for PlugView

{ + unsafe fn onFDIsSet(&self, _fd: FileDescriptor) { + let state = unsafe { &mut *self.main_thread_state.get() }; + state.view.as_mut().unwrap().poll(); + } + } + + impl ITimerHandlerTrait for PlugView

{ + unsafe fn onTimer(&self) { + let state = unsafe { &mut *self.main_thread_state.get() }; + state.view.as_mut().unwrap().poll(); } } + + impl Class for PlugView

{ + type Interfaces = (IEventHandler, ITimerHandler, IPlugView); + } } -impl Class for PlugView

{ - type Interfaces = (IPlugView,); +impl PlugView

{ + pub fn new(main_thread_state: &Arc>>) -> ComPtr { + let view = ComWrapper::new(PlugView { + main_thread_state: main_thread_state.clone(), + self_ptr: Cell::new(None), + }); + let com_ptr = view.to_com_ptr::().unwrap(); + let raw_ptr = com_ptr.as_ptr(); + view.self_ptr.set(Some(raw_ptr)); + com_ptr + } } impl IPlugViewTrait for PlugView

{ @@ -108,12 +141,71 @@ impl IPlugViewTrait for PlugView

{ let view = main_thread_state.plugin.view(host, &parent); main_thread_state.view = Some(view); + #[cfg(target_os = "linux")] + { + use vst3::Steinberg::Linux::*; + + let Some(frame) = main_thread_state.view_host.plug_frame.borrow().clone() else { + return kNotInitialized; + }; + + if let Some(run_loop) = frame.cast::() { + if let Some(ptr) = self.self_ptr.get() { + if let Some(com_ref) = unsafe {ComRef::from_raw(ptr)} { + if let Some(timer_handler_ptr) = com_ref.cast::() { + unsafe { + run_loop.registerTimer(timer_handler_ptr.as_ptr(), 16); + } + if let Some(fd) = + (*self.main_thread_state.get()).view.as_ref().unwrap().file_descriptor() { + if let Some(event_handler_ptr) = com_ref.cast::() { + run_loop.registerEventHandler(event_handler_ptr.as_ptr(), fd); + } + } + } + } + + } + } + } + kResultOk } unsafe fn removed(&self) -> tresult { let main_thread_state = &mut *self.main_thread_state.get(); + #[cfg(target_os = "linux")] + { + use vst3::Steinberg::Linux::*; + + let Some(frame) = main_thread_state.view_host.plug_frame.borrow().clone() else { + return kNotInitialized; + }; + + if let Some(run_loop) = frame.cast::() { + if let Some(ptr) = self.self_ptr.get() { + if let Some(com_ref) = unsafe {ComRef::from_raw(ptr)} { + if let Some(timer_handler_ptr) = com_ref.cast::() { + unsafe { + run_loop.unregisterTimer(timer_handler_ptr.as_ptr()); + } + } + // confirm an event handler actually was registered + // before trying to unregister (we only register if we can get a + // file descriptor) + if (*self.main_thread_state.get()).view.as_ref().unwrap().file_descriptor().is_some() { + if let Some(event_handler_ptr) = com_ref.cast::() { + unsafe { + run_loop.unregisterEventHandler(event_handler_ptr.as_ptr()); + } + } + } + } + } + } + } + main_thread_state.view = None; kResultOk @@ -162,7 +254,13 @@ impl IPlugViewTrait for PlugView

{ } unsafe fn setFrame(&self, _frame: *mut IPlugFrame) -> tresult { - kNotImplemented + let main_thread_state = &mut *self.main_thread_state.get(); + + if let Some(frame) = ComRef::from_raw(_frame) { + main_thread_state.view_host.plug_frame.replace(Some(frame.to_com_ptr())); + } + + kResultOk } unsafe fn canResize(&self) -> tresult { diff --git a/src/view.rs b/src/view.rs index d73cdbdb..d9240d8d 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,5 +1,6 @@ use std::ffi::{c_ulong, c_void}; use std::marker::PhantomData; +use std::os::raw::c_int; use std::rc::Rc; use crate::params::{ParamId, ParamValue}; @@ -67,6 +68,10 @@ pub struct Size { pub trait View: Sized + 'static { fn size(&self) -> Size; fn param_changed(&mut self, id: ParamId, value: ParamValue); + #[cfg(target_os = "linux")] + fn file_descriptor(&self) -> Option; + #[cfg(target_os = "linux")] + fn poll(&mut self); } pub struct NoView; @@ -80,4 +85,10 @@ impl View for NoView { } fn param_changed(&mut self, _id: ParamId, _value: ParamValue) {} + + fn file_descriptor(&self) -> Option { + None + } + + fn poll(&mut self) {} }