1
0
Fork 0
nih-plug/nih_plug_egui/src/lib.rs

180 lines
6.5 KiB
Rust
Raw Normal View History

2022-02-06 10:22:33 +11:00
//! [egui](https://github.com/emilk/egui) editor support for NIH plug.
//!
//! TODO: Proper usage example, for now check out the gain_gui example
2022-02-06 10:22:33 +11:00
use baseview::gl::GlConfig;
2022-02-06 10:22:33 +11:00
use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy};
use crossbeam::atomic::AtomicCell;
2022-02-28 02:49:18 +11:00
use egui::Context;
use egui_baseview::EguiWindow;
use nih_plug::prelude::{Editor, GuiContext, ParamSetter, ParentWindowHandle};
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
2022-02-06 10:22:33 +11:00
use std::sync::Arc;
/// Re-export for convenience.
2022-02-06 10:54:13 +11:00
pub use egui;
2022-02-06 10:22:33 +11:00
pub mod widgets;
/// Create an [`Editor`] instance using an [`egui`][::egui] 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 [`EguiState`] 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 [`EguiState::from_size()`].
pub fn create_egui_editor<T, U>(
egui_state: Arc<EguiState>,
user_state: T,
update: U,
) -> Option<Box<dyn Editor>>
2022-02-06 10:22:33 +11:00
where
T: 'static + Send + Sync,
2022-02-28 02:49:18 +11:00
U: Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync,
2022-02-06 10:22:33 +11:00
{
Some(Box::new(EguiEditor {
egui_state,
user_state: Arc::new(RwLock::new(user_state)),
update: Arc::new(update),
scaling_factor: AtomicCell::new(None),
}))
2022-02-06 10:22:33 +11:00
}
// 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 EguiState {
size: AtomicCell<(u32, u32)>,
open: AtomicBool,
}
impl EguiState {
/// Initialize the GUI's state. This value can be passed to [`create_egui_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<EguiState> {
Arc::new(EguiState {
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 calls an egui draw loop.
struct EguiEditor<T> {
egui_state: Arc<EguiState>,
/// The plugin's state. This is kept in between editor openenings.
user_state: Arc<RwLock<T>>,
2022-02-28 02:49:18 +11:00
update: Arc<dyn Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync>,
/// 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<Option<f32>>,
2022-02-06 10:22:33 +11:00
}
impl<T> Editor for EguiEditor<T>
where
T: 'static + Send + Sync,
{
fn spawn(
&self,
parent: ParentWindowHandle,
context: Arc<dyn GuiContext>,
2022-03-04 01:39:17 +11:00
) -> Box<dyn std::any::Any + Send + Sync> {
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),
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| {
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.
// TODO: Are there other useful parts of this queue we could pass to thep lugin?
queue.request_repaint();
(update)(egui_ctx, &setter, &mut state.write());
},
)
.expect("We provided an OpenGL config, did we not?");
self.egui_state.open.store(true, Ordering::Release);
Box::new(EguiEditorHandle {
egui_state: self.egui_state.clone(),
window,
})
}
2022-02-06 10:22:33 +11:00
fn size(&self) -> (u32, u32) {
self.egui_state.size()
2022-02-06 10:22:33 +11:00
}
fn set_scale_factor(&self, factor: f32) -> bool {
self.scaling_factor.store(Some(factor));
true
}
2022-02-06 10:22:33 +11:00
}
/// The window handle used for [`EguiEditor`].
struct EguiEditorHandle {
egui_state: Arc<EguiState>,
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 {}
unsafe impl Sync 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();
}
}