diff --git a/Cargo.lock b/Cargo.lock index 9312f154..80d6938d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1938,6 +1938,7 @@ name = "nih_plug_iced" version = "0.0.0" dependencies = [ "baseview", + "crossbeam", "iced_baseview", "nih_plug", ] diff --git a/nih_plug_egui/Cargo.toml b/nih_plug_egui/Cargo.toml index fd32459a..5edecdf3 100644 --- a/nih_plug_egui/Cargo.toml +++ b/nih_plug_egui/Cargo.toml @@ -7,6 +7,10 @@ license = "ISC" description = "An adapter to use egui GUIs with NIH-plug" +[features] +default = ["opengl"] +opengl = [] + [dependencies] nih_plug = { path = ".." } diff --git a/nih_plug_egui/src/lib.rs b/nih_plug_egui/src/lib.rs index 1ee28c2d..4ade9c16 100644 --- a/nih_plug_egui/src/lib.rs +++ b/nih_plug_egui/src/lib.rs @@ -12,6 +12,9 @@ use parking_lot::RwLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +#[cfg(not(feature = "opengl"))] +compile_error!("There's currently no software rendering support for egui"); + /// Re-export for convenience. pub use egui; @@ -111,6 +114,8 @@ where scale: scaling_factor .map(|factor| WindowScalePolicy::ScaleFactor(factor as f64)) .unwrap_or(WindowScalePolicy::SystemScaleFactor), + + #[cfg(feature = "opengl")] gl_config: Some(GlConfig { version: (3, 2), red_bits: 8, diff --git a/nih_plug_iced/Cargo.toml b/nih_plug_iced/Cargo.toml index b536c922..1e5b78b2 100644 --- a/nih_plug_iced/Cargo.toml +++ b/nih_plug_iced/Cargo.toml @@ -12,7 +12,9 @@ default = ["wgpu"] # Use wgpu rendering, which translates to Vulkan, Metal, or Direct3D12 depending # on the platform. -wgpu = ["iced_baseview/wgpu"] +# NOTE: The OpenGL support in baseview is not used, this is just a workaround +# for a rust analyzer bug. See the comment in lib.rs. +wgpu = ["iced_baseview/wgpu", "baseview/opengl"] # Use OpenGL instead of wgpu for the rendering. This should increase platform # compatibility at the cost of some iced features not being available. opengl = ["iced_baseview/glow"] @@ -54,6 +56,7 @@ smol = ["iced_baseview/smol"] nih_plug = { path = ".." } baseview = { git = "https://github.com/robbert-vdh/baseview.git", branch = "feature/mouse-event-modifiers" } +crossbeam = "0.8" # Upstream doesn't work with the current iced version, this branch also contains # additional features iced_baseview = { git = "https://github.com/robbert-vdh/iced_baseview.git", branch = "feature/update-dependencies", default_features = false } diff --git a/nih_plug_iced/src/lib.rs b/nih_plug_iced/src/lib.rs index 36ac3aa4..e9dd0ff3 100644 --- a/nih_plug_iced/src/lib.rs +++ b/nih_plug_iced/src/lib.rs @@ -2,5 +2,186 @@ //! //! TODO: Proper usage example, for now check out the gain_gui example +use baseview::{Size, WindowOpenOptions, WindowScalePolicy}; +use crossbeam::atomic::AtomicCell; +use nih_plug::prelude::{Editor, GuiContext, ParamSetter, ParentWindowHandle}; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + /// Re-export for convenience. pub use iced_baseview::*; + +/// FIXME: Document how this works once everything actually works. The below comment is from the +/// egui version. +/// +/// Create an [`Editor`] instance using an [`iced`][::iced] GUI. Using the user state parameter is +/// optional, but it can be useful for keeping track of some temporary GUI-only settings. See the +/// `gui_gain` example for more information on how to use this. The [`IcedState`] passed to this +/// function contains the GUI's intitial size, and this is kept in sync whenever the GUI gets +/// resized. You can also use this to know if the GUI is open, so you can avoid performing +/// potentially expensive calculations while the GUI is not open. If you want this size to be +/// persisted when restoring a plugin instance, then you can store it in a `#[persist = "key"]` +/// field on your parameters struct. +/// +/// See [`IcedState::from_size()`]. +pub fn create_iced_editor( + iced_state: Arc, + initialization_flags: A::Flags, +) -> Option> +where + A: Application + 'static + Send + Sync, + A::Flags: Clone + Sync, +{ + Some(Box::new(IcedEditor:: { + iced_state, + initialization_flags, + + scaling_factor: AtomicCell::new(None), + + _phantom: PhantomData, + })) +} + +// TODO: Once we add resizing, we may want to be able to remember the GUI size. In that case we need +// to make this serializable (only restoring the size of course) so it can be persisted. +pub struct IcedState { + size: AtomicCell<(u32, u32)>, + open: AtomicBool, +} + +impl IcedState { + /// Initialize the GUI's state. This value can be passed to [`create_iced_editor()`]. The window + /// size is in logical pixels, so before it is multiplied by the DPI scaling factor. + pub fn from_size(width: u32, height: u32) -> Arc { + Arc::new(IcedState { + size: AtomicCell::new((width, height)), + open: AtomicBool::new(false), + }) + } + + /// Return a `(width, height)` pair for the current size of the GUI in logical pixels. + pub fn size(&self) -> (u32, u32) { + self.size.load() + } + + /// Whether the GUI is currently visible. + // Called `is_open()` instead of `open()` to avoid the ambiguity. + pub fn is_open(&self) -> bool { + self.open.load(Ordering::Acquire) + } +} + +/// An [`Editor`] implementation that renders an iced [`Application`]. +struct IcedEditor +where + A: Application + 'static + Send + Sync, + A::Flags: Clone + Sync, +{ + iced_state: Arc, + initialization_flags: A::Flags, + + // FIXME: + // /// The plugin's state. This is kept in between editor openenings. + // user_state: Arc>, + // update: Arc, + // + /// The scaling factor reported by the host, if any. On macOS this will never be set and we + /// should use the system scaling factor instead. + scaling_factor: AtomicCell>, + + _phantom: PhantomData, +} + +impl Editor for IcedEditor +where + A: Application + 'static + Send + Sync, + A::Flags: Clone + Sync, +{ + fn spawn( + &self, + parent: ParentWindowHandle, + context: Arc, + ) -> Box { + // FIXME: Somehow get the context/parametersetter to the GUI. Another trait that adds a + // `set_context()` would be the easiest way but perhaps not the cleanest. + + let (unscaled_width, unscaled_height) = self.iced_state.size(); + let scaling_factor = self.scaling_factor.load(); + // TODO: iced_baseview does not have gracefuly error handling for context creation failures. + // This will panic if the context could not be created. + let window = IcedWindow::::open_parented( + &parent, + Settings { + window: WindowOpenOptions { + title: String::from("iced window"), + // Baseview should be doing the DPI scaling for us + size: Size::new(unscaled_width as f64, unscaled_height as f64), + // NOTE: For some reason passing 1.0 here causes the UI to be scaled on macOS but + // not the mouse events. + scale: scaling_factor + .map(|factor| WindowScalePolicy::ScaleFactor(factor as f64)) + .unwrap_or(WindowScalePolicy::SystemScaleFactor), + + #[cfg(feature = "opengl")] + gl_config: Some(baseview::gl::GlConfig { + // FIXME: glow_glyph forgot to add an `#extension`, so this won't work under + // OpenGL 3.2 at the moment. With that change applied this should work on + // OpenGL 3.2/macOS. + version: (3, 3), + red_bits: 8, + blue_bits: 8, + green_bits: 8, + alpha_bits: 8, + depth_bits: 24, + stencil_bits: 8, + samples: None, + srgb: true, + double_buffer: true, + vsync: true, + ..Default::default() + }), + // FIXME: Rust analyzer always thinks baseview/opengl is enabled even if we + // don't explicitly enable it, so you'd get a compile error if this line + // is missing + #[cfg(not(feature = "opengl"))] + gl_config: None, + }, + flags: self.initialization_flags.clone(), + }, + ); + + self.iced_state.open.store(true, Ordering::Release); + Box::new(IcedEditorHandle { + iced_state: self.iced_state.clone(), + window, + }) + } + + fn size(&self) -> (u32, u32) { + self.iced_state.size() + } + + fn set_scale_factor(&self, factor: f32) -> bool { + self.scaling_factor.store(Some(factor)); + true + } +} + +/// The window handle used for [`IcedEditor`]. +struct IcedEditorHandle { + iced_state: Arc, + window: iced_baseview::WindowHandle, +} + +/// The window handle enum stored within 'WindowHandle' contains raw pointers. Is there a way around +/// having this requirement? +unsafe impl Send for IcedEditorHandle {} +unsafe impl Sync for IcedEditorHandle {} + +impl Drop for IcedEditorHandle { + fn drop(&mut self) { + self.iced_state.open.store(false, Ordering::Release); + self.window.close_window(); + } +}