diff --git a/nih_plug_egui/src/editor.rs b/nih_plug_egui/src/editor.rs new file mode 100644 index 00000000..5f40e3a6 --- /dev/null +++ b/nih_plug_egui/src/editor.rs @@ -0,0 +1,127 @@ +//! An [`Editor`] implementation for egui. + +use baseview::gl::GlConfig; +use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy}; +use crossbeam::atomic::AtomicCell; +use egui::Context; +use egui_baseview::EguiWindow; +use nih_plug::prelude::{Editor, GuiContext, ParamSetter, ParentWindowHandle}; +use parking_lot::RwLock; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use crate::EguiState; + +/// An [`Editor`] implementation that calls an egui draw loop. +pub(crate) struct EguiEditor { + pub(crate) egui_state: Arc, + /// The plugin's state. This is kept in between editor openenings. + pub(crate) user_state: Arc>, + + /// The user's build function. Applied once at the start of the application. + pub(crate) build: Arc, + /// The user's update function. + pub(crate) 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. + pub(crate) scaling_factor: AtomicCell>, +} + +impl Editor for EguiEditor +where + T: 'static + Send + Sync, +{ + fn spawn( + &self, + parent: ParentWindowHandle, + context: Arc, + ) -> Box { + let build = self.build.clone(); + let update = self.update.clone(); + let state = self.user_state.clone(); + + let (unscaled_width, unscaled_height) = self.egui_state.size(); + let scaling_factor = self.scaling_factor.load(); + let window = EguiWindow::open_parented( + &parent, + WindowOpenOptions { + title: String::from("egui 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(GlConfig { + version: (3, 2), + 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() + }), + }, + state, + move |egui_ctx, _queue, state| build(egui_ctx, &mut state.write()), + move |egui_ctx, _queue, state| { + let setter = ParamSetter::new(context.as_ref()); + + // For now, just always redraw. Most plugin GUIs have meters, and those almost always + // need a redraw. Later we can try to be a bit more sophisticated about this. Without + // this we would also have a blank GUI when it gets first opened because most DAWs open + // their GUI while the window is still unmapped. + egui_ctx.request_repaint(); + (update)(egui_ctx, &setter, &mut state.write()); + }, + ); + + self.egui_state.open.store(true, Ordering::Release); + Box::new(EguiEditorHandle { + egui_state: self.egui_state.clone(), + window, + }) + } + + fn size(&self) -> (u32, u32) { + self.egui_state.size() + } + + fn set_scale_factor(&self, factor: f32) -> bool { + self.scaling_factor.store(Some(factor)); + true + } + + fn param_values_changed(&self) { + // As mentioned above, for now we'll always force a redraw to allow meter widgets to work + // correctly. In the future we can use an `Arc` and only force a redraw when + // that boolean is set. + } +} + +/// The window handle used for [`EguiEditor`]. +struct EguiEditorHandle { + egui_state: Arc, + window: WindowHandle, +} + +/// The window handle enum stored within 'WindowHandle' contains raw pointers. Is there a way around +/// having this requirement? +unsafe impl Send for EguiEditorHandle {} + +impl Drop for EguiEditorHandle { + fn drop(&mut self) { + self.egui_state.open.store(false, Ordering::Release); + // XXX: This should automatically happen when the handle gets dropped, but apparently not + self.window.close(); + } +} diff --git a/nih_plug_egui/src/lib.rs b/nih_plug_egui/src/lib.rs index b84b9607..3938a6c7 100644 --- a/nih_plug_egui/src/lib.rs +++ b/nih_plug_egui/src/lib.rs @@ -5,13 +5,10 @@ // See the comment in the main `nih_plug` crate #![allow(clippy::type_complexity)] -use baseview::gl::GlConfig; -use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy}; use crossbeam::atomic::AtomicCell; use egui::Context; -use egui_baseview::EguiWindow; use nih_plug::params::persist::PersistentField; -use nih_plug::prelude::{Editor, GuiContext, ParamSetter, ParentWindowHandle}; +use nih_plug::prelude::{Editor, ParamSetter}; use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -23,6 +20,7 @@ compile_error!("There's currently no software rendering support for egui"); /// Re-export for convenience. pub use egui; +mod editor; pub mod widgets; /// Create an [`Editor`] instance using an [`egui`][::egui] GUI. Using the user state parameter is @@ -46,7 +44,7 @@ where B: Fn(&Context, &mut T) + 'static + Send + Sync, U: Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync, { - Some(Box::new(EguiEditor { + Some(Box::new(editor::EguiEditor { egui_state, user_state: Arc::new(RwLock::new(user_state)), build: Arc::new(build), @@ -107,117 +105,3 @@ impl EguiState { self.open.load(Ordering::Acquire) } } - -/// An [`Editor`] implementation that calls an egui draw loop. -struct EguiEditor { - egui_state: Arc, - /// The plugin's state. This is kept in between editor openenings. - user_state: Arc>, - - /// The user's build function. Applied once at the start of the application. - build: Arc, - /// The user's update function. - 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>, -} - -impl Editor for EguiEditor -where - T: 'static + Send + Sync, -{ - fn spawn( - &self, - parent: ParentWindowHandle, - context: Arc, - ) -> Box { - let build = self.build.clone(); - let update = self.update.clone(); - let state = self.user_state.clone(); - - let (unscaled_width, unscaled_height) = self.egui_state.size(); - let scaling_factor = self.scaling_factor.load(); - let window = EguiWindow::open_parented( - &parent, - WindowOpenOptions { - title: String::from("egui 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(GlConfig { - version: (3, 2), - 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() - }), - }, - state, - move |egui_ctx, _queue, state| build(egui_ctx, &mut state.write()), - move |egui_ctx, _queue, state| { - let setter = ParamSetter::new(context.as_ref()); - - // For now, just always redraw. Most plugin GUIs have meters, and those almost always - // need a redraw. Later we can try to be a bit more sophisticated about this. Without - // this we would also have a blank GUI when it gets first opened because most DAWs open - // their GUI while the window is still unmapped. - egui_ctx.request_repaint(); - (update)(egui_ctx, &setter, &mut state.write()); - }, - ); - - self.egui_state.open.store(true, Ordering::Release); - Box::new(EguiEditorHandle { - egui_state: self.egui_state.clone(), - window, - }) - } - - fn size(&self) -> (u32, u32) { - self.egui_state.size() - } - - fn set_scale_factor(&self, factor: f32) -> bool { - self.scaling_factor.store(Some(factor)); - true - } - - fn param_values_changed(&self) { - // As mentioned above, for now we'll always force a redraw to allow meter widgets to work - // correctly. In the future we can use an `Arc` and only force a redraw when - // that boolean is set. - } -} - -/// The window handle used for [`EguiEditor`]. -struct EguiEditorHandle { - egui_state: Arc, - window: WindowHandle, -} - -/// The window handle enum stored within 'WindowHandle' contains raw pointers. Is there a way around -/// having this requirement? -unsafe impl Send for EguiEditorHandle {} - -impl Drop for EguiEditorHandle { - fn drop(&mut self) { - self.egui_state.open.store(false, Ordering::Release); - // XXX: This should automatically happen when the handle gets dropped, but apparently not - self.window.close(); - } -}