diff --git a/Cargo.toml b/Cargo.toml index 30c740cd..3c38e849 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ objc2-app-kit = { version = "0.3.2", default-features = false, features = [ ] } [workspace] -members = ["examples/open_parented", "examples/open_window", "examples/render_femtovg", "examples/render_wgpu"] +members = ["examples/open_parented", "examples/open_window", "examples/plugin_clack", "examples/render_femtovg", "examples/render_wgpu"] [lints.clippy] missing-safety-doc = "allow" diff --git a/examples/plugin_clack/Cargo.toml b/examples/plugin_clack/Cargo.toml new file mode 100644 index 00000000..1402c072 --- /dev/null +++ b/examples/plugin_clack/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "plugin_clack" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +clack-plugin = "0.1.0" +clack-extensions = { version = "0.1.0", features = ["gui", "clack-plugin", "raw-window-handle_06"] } +baseview = { path = "../..", features = ["opengl"] } +softbuffer = "0.4.8" +raw-window-handle = "0.6.2" diff --git a/examples/plugin_clack/src/audio.rs b/examples/plugin_clack/src/audio.rs new file mode 100644 index 00000000..8d5b9bf5 --- /dev/null +++ b/examples/plugin_clack/src/audio.rs @@ -0,0 +1,31 @@ +use crate::ExamplePluginMainThread; +use clack_plugin::prelude::*; + +pub struct ExamplePluginAudioProcessor; + +impl<'a> PluginAudioProcessor<'a, (), ExamplePluginMainThread> for ExamplePluginAudioProcessor { + fn activate( + _host: HostAudioProcessorHandle<'a>, _main_thread: &mut ExamplePluginMainThread, + _shared: &'a (), _audio_config: PluginAudioConfiguration, + ) -> Result { + Ok(Self) + } + + fn process( + &mut self, _process: Process, mut audio: Audio, _events: Events, + ) -> Result { + for mut port in audio.port_pairs() { + let channels = port.channels()?.into_f32().expect("Expected f32 channels"); + + for channel_pair in channels { + match channel_pair { + ChannelPair::OutputOnly(o) => o.fill(0.0), + ChannelPair::InputOutput(i, o) => o.copy_from_slice(i), + _ => {} + } + } + } + + Ok(ProcessStatus::Continue) + } +} diff --git a/examples/plugin_clack/src/gui.rs b/examples/plugin_clack/src/gui.rs new file mode 100644 index 00000000..62fa72e2 --- /dev/null +++ b/examples/plugin_clack/src/gui.rs @@ -0,0 +1,99 @@ +use crate::window_handler::OpenWindowExample; +use crate::ExamplePluginMainThread; +use baseview::dpi::PhysicalSize; +use baseview::gl::GlConfig; +use baseview::{Window, WindowHandle, WindowOpenOptions}; +use clack_extensions::gui::{ + GuiApiType, GuiConfiguration, GuiResizeHints, GuiSize, PluginGuiImpl, Window as ClapWindow, +}; +use clack_plugin::plugin::PluginError; + +#[allow(deprecated)] +use raw_window_handle::HasRawWindowHandle; + +pub struct ExamplePluginGui { + handle: WindowHandle, +} + +impl PluginGuiImpl for ExamplePluginMainThread { + fn is_api_supported(&mut self, configuration: GuiConfiguration) -> bool { + !configuration.is_floating + && Some(configuration.api_type) == GuiApiType::default_for_current_platform() + } + + fn get_preferred_api(&mut self) -> Option> { + Some(GuiConfiguration { + api_type: GuiApiType::default_for_current_platform()?, + is_floating: false, + }) + } + + fn create(&mut self, _configuration: GuiConfiguration) -> Result<(), PluginError> { + // Delay creation until set_parent + Ok(()) + } + + fn destroy(&mut self) { + let Some(gui) = self.gui.take() else { return }; + + gui.handle.close() + } + + fn set_scale(&mut self, _scale: f64) -> Result<(), PluginError> { + // Unsupported + Ok(()) + } + + fn get_size(&mut self) -> Option { + // Unsupported + Some(GuiSize { width: 400, height: 200 }) + } + + fn can_resize(&mut self) -> bool { + false // Non-resizeable windows not supported yet + } + + fn get_resize_hints(&mut self) -> Option { + None // Not supported yet + } + + fn adjust_size(&mut self, _size: GuiSize) -> Option { + None // Not supported yet + } + + fn set_size(&mut self, _size: GuiSize) -> Result<(), PluginError> { + Ok(()) // Not supported yet + } + + #[allow(deprecated)] + fn set_parent(&mut self, window: ClapWindow) -> Result<(), PluginError> { + let parent = window.raw_window_handle()?; + let parent = unsafe { raw_window_handle::WindowHandle::borrow_raw(parent) }; + + let options = WindowOpenOptions::new() + .with_size(PhysicalSize::new(400, 200)) + .with_gl_config(GlConfig::default()); + + let window = Window::open_parented(&parent, options, OpenWindowExample::new); + + self.gui = Some(ExamplePluginGui { handle: window }); + + Ok(()) + } + + fn set_transient(&mut self, _window: ClapWindow) -> Result<(), PluginError> { + unimplemented!() // Not supported yet + } + + fn suggest_title(&mut self, _title: &str) { + // Not supported yet + } + + fn show(&mut self) -> Result<(), PluginError> { + Ok(()) // Not supported yet + } + + fn hide(&mut self) -> Result<(), PluginError> { + Ok(()) // Not supported yet + } +} diff --git a/examples/plugin_clack/src/lib.rs b/examples/plugin_clack/src/lib.rs new file mode 100644 index 00000000..31893f06 --- /dev/null +++ b/examples/plugin_clack/src/lib.rs @@ -0,0 +1,54 @@ +use crate::audio::ExamplePluginAudioProcessor; +use crate::gui::ExamplePluginGui; +use clack_extensions::gui::PluginGui; +use clack_plugin::prelude::*; + +mod audio; +mod gui; +mod window_handler; + +/// The type that represents our plugin in Clack. +/// +/// This is what implements the [`Plugin`] trait, where all the other subtypes are attached. +pub struct ExamplePlugin; + +impl Plugin for ExamplePlugin { + type AudioProcessor<'a> = ExamplePluginAudioProcessor; + type Shared<'a> = (); + type MainThread<'a> = ExamplePluginMainThread; + + fn declare_extensions(builder: &mut PluginExtensions, _shared: Option<&()>) { + builder.register::(); + } +} + +impl DefaultPluginFactory for ExamplePlugin { + fn get_descriptor() -> PluginDescriptor { + use clack_plugin::plugin::features::*; + + PluginDescriptor::new("org.rust-audio.clack.gain-egui", "Clack Gain EGUI Example") + .with_features([AUDIO_EFFECT, STEREO]) + } + + fn new_shared(_host: HostSharedHandle<'_>) -> Result, PluginError> { + Ok(()) + } + + fn new_main_thread<'a>( + _host: HostMainThreadHandle<'a>, _shared: &'a Self::Shared<'a>, + ) -> Result, PluginError> { + Ok(Self::MainThread { gui: None }) + } +} + +/// The data that belongs to the main thread of our plugin. +pub struct ExamplePluginMainThread { + /// The plugin's GUI state and context + gui: Option, +} + +impl<'a> PluginMainThread<'a, ()> for ExamplePluginMainThread { + fn on_main_thread(&mut self) {} +} + +clack_export_entry!(SinglePluginEntry); diff --git a/examples/plugin_clack/src/window_handler.rs b/examples/plugin_clack/src/window_handler.rs new file mode 100644 index 00000000..726b2bab --- /dev/null +++ b/examples/plugin_clack/src/window_handler.rs @@ -0,0 +1,143 @@ +use baseview::dpi::PhysicalPosition; +use baseview::{Event, EventStatus, MouseEvent, WindowContext, WindowHandler, WindowSize}; +use std::cell::{Cell, RefCell}; +use std::num::NonZeroU32; + +pub struct OpenWindowExample { + window_context: WindowContext, + + surface: RefCell>, + mouse_pos: Cell>, + is_cursor_inside: Cell, + damaged: Cell, +} + +impl WindowHandler for OpenWindowExample { + fn resized(&self, new_size: WindowSize) { + println!("Resized: {new_size:?}"); + + if let (Some(width), Some(height)) = + (NonZeroU32::new(new_size.physical.width), NonZeroU32::new(new_size.physical.height)) + { + self.surface.borrow_mut().resize(width, height).unwrap(); + self.damaged.set(true); + } + } + + fn on_frame(&self) { + if !self.damaged.get() { + return; + } + + let mut surface = self.surface.borrow_mut(); + let mut pixels = surface.buffer_mut().unwrap(); + let size = self.window_context.size(); + let scale_factor = self.window_context.scale_factor(); + let (width, height) = (size.physical.width, size.physical.height); + + for index in 0..(width * height) { + let x = index % width; + let y = index / width; + + let red = ((x as f32 / width as f32) * 255.0) as u32; + let green = ((y as f32 / height as f32) * 255.0) as u32; + let blue = (((x * y) as f64 / scale_factor) as u32) % 255; + + pixels[index as usize] = blue | (green << 8) | (red << 16) | 0xFF000000; + } + + for x in 0..width { + // Green line on top + let y = 0; + let index = y * width + x; + pixels[index as usize] = if (x % 10) < 5 { 0xFF00FF00 } else { 0xFF000000 }; + + // Magenta line on bottom + let y = height - 1; + let index = y * width + x; + pixels[index as usize] = if (x % 10) < 5 { 0xFFFF00FF } else { 0xFF000000 }; + } + + for y in 0..height { + // Green line on right + let x = width - 1; + let index = y * width + x; + pixels[index as usize] = if (y % 10) < 5 { 0xFF00FF00 } else { 0xFF000000 }; + + // Magenta line on left + let x = 0; + let index = y * width + x; + pixels[index as usize] = if (y % 10) < 5 { 0xFFFF00FF } else { 0xFF000000 }; + } + + if self.is_cursor_inside.get() { + let rect_size = (25.0 * scale_factor) as i32; + let mouse_pos = self.mouse_pos.get().cast::(); + + let rect_x_start = (mouse_pos.x - rect_size).clamp(0, width as i32) as u32; + let rect_x_end = (mouse_pos.x + rect_size).clamp(0, width as i32) as u32; + let rect_y_start = (mouse_pos.y - rect_size).clamp(0, height as i32) as u32; + let rect_y_end = (mouse_pos.y + rect_size).clamp(0, height as i32) as u32; + + for x in rect_x_start..rect_x_end { + for y in rect_y_start..rect_y_end { + let index = y * width + x; + pixels[index as usize] = 0xFF00FF00; + } + } + } + + pixels.present().unwrap(); + self.damaged.set(false); + } + + fn on_event(&self, event: Event) -> EventStatus { + match event { + Event::Mouse(MouseEvent::CursorMoved { position, .. }) => { + self.mouse_pos.set(position); + self.damaged.set(true); + } + Event::Mouse(MouseEvent::CursorEntered) => { + self.is_cursor_inside.set(true); + self.damaged.set(true); + } + Event::Mouse(MouseEvent::CursorLeft) => { + self.is_cursor_inside.set(false); + self.damaged.set(true); + } + _ => {} + } + + log_event(&event); + + EventStatus::Captured + } +} + +impl OpenWindowExample { + pub fn new(window: WindowContext) -> Self { + let ctx = softbuffer::Context::new(window.clone()).unwrap(); + let mut surface = softbuffer::Surface::new(&ctx, window.clone()).unwrap(); + let size = window.size().physical; + surface + .resize(NonZeroU32::new(size.width).unwrap(), NonZeroU32::new(size.height).unwrap()) + .unwrap(); + + OpenWindowExample { + window_context: window, + surface: surface.into(), + mouse_pos: PhysicalPosition::new(0., 0.).into(), + is_cursor_inside: false.into(), + damaged: true.into(), + } + } +} + +fn log_event(event: &Event) { + match event { + Event::Mouse(e) => println!("Mouse event: {:?}", e), + Event::Keyboard(e) => println!("Keyboard event: {:?}", e), + Event::Window(e) => println!("Window event: {:?}", e), + _ => {} + } +}