2022-02-06 00:22:33 +01:00
|
|
|
//! [egui](https://github.com/emilk/egui) editor support for NIH plug.
|
|
|
|
//!
|
2022-02-08 23:25:56 +01:00
|
|
|
//! TODO: Proper usage example, for now check out the gain_gui example
|
2022-02-06 00:22:33 +01:00
|
|
|
|
2022-07-01 15:50:23 +02:00
|
|
|
// See the comment in the main `nih_plug` crate
|
|
|
|
#![allow(clippy::type_complexity)]
|
|
|
|
|
2022-02-07 21:58:59 +01:00
|
|
|
use baseview::gl::GlConfig;
|
2022-02-06 00:22:33 +01:00
|
|
|
use baseview::{Size, WindowHandle, WindowOpenOptions, WindowScalePolicy};
|
2022-02-08 20:38:10 +01:00
|
|
|
use crossbeam::atomic::AtomicCell;
|
2022-02-27 16:49:18 +01:00
|
|
|
use egui::Context;
|
2022-02-07 21:58:59 +01:00
|
|
|
use egui_baseview::EguiWindow;
|
2022-10-20 12:26:12 +02:00
|
|
|
use nih_plug::params::persist::PersistentField;
|
2022-03-03 23:23:51 +01:00
|
|
|
use nih_plug::prelude::{Editor, GuiContext, ParamSetter, ParentWindowHandle};
|
2022-02-06 17:12:57 +01:00
|
|
|
use parking_lot::RwLock;
|
2022-07-13 23:13:15 +02:00
|
|
|
use serde::{Deserialize, Serialize};
|
2022-02-08 20:16:39 +01:00
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
2022-02-06 00:22:33 +01:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
2022-03-12 23:15:33 +01:00
|
|
|
#[cfg(not(feature = "opengl"))]
|
|
|
|
compile_error!("There's currently no software rendering support for egui");
|
|
|
|
|
2022-02-06 00:22:33 +01:00
|
|
|
/// Re-export for convenience.
|
2022-02-06 00:54:13 +01:00
|
|
|
pub use egui;
|
2022-02-06 00:22:33 +01:00
|
|
|
|
2022-02-08 23:25:56 +01:00
|
|
|
pub mod widgets;
|
|
|
|
|
2022-03-03 23:05:01 +01:00
|
|
|
/// 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.
|
2022-02-08 20:16:39 +01:00
|
|
|
///
|
2022-03-03 23:05:01 +01:00
|
|
|
/// See [`EguiState::from_size()`].
|
2022-10-20 14:03:55 +02:00
|
|
|
pub fn create_egui_editor<T, B, U>(
|
2022-02-08 20:16:39 +01:00
|
|
|
egui_state: Arc<EguiState>,
|
|
|
|
user_state: T,
|
2022-10-20 14:03:55 +02:00
|
|
|
build: B,
|
2022-02-06 17:12:57 +01:00
|
|
|
update: U,
|
2022-02-06 14:02:55 +01:00
|
|
|
) -> Option<Box<dyn Editor>>
|
2022-02-06 00:22:33 +01:00
|
|
|
where
|
2022-02-06 17:12:57 +01:00
|
|
|
T: 'static + Send + Sync,
|
2022-10-20 14:03:55 +02:00
|
|
|
B: Fn(&Context, &mut T) + 'static + Send + Sync,
|
2022-02-27 16:49:18 +01:00
|
|
|
U: Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync,
|
2022-02-06 00:22:33 +01:00
|
|
|
{
|
2022-02-06 17:12:57 +01:00
|
|
|
Some(Box::new(EguiEditor {
|
2022-02-08 20:16:39 +01:00
|
|
|
egui_state,
|
|
|
|
user_state: Arc::new(RwLock::new(user_state)),
|
2022-10-20 14:03:55 +02:00
|
|
|
build: Arc::new(build),
|
2022-02-06 17:12:57 +01:00
|
|
|
update: Arc::new(update),
|
2022-03-05 13:37:35 +01:00
|
|
|
|
2022-04-27 18:33:08 +02:00
|
|
|
// TODO: We can't get the size of the window when baseview does its own scaling, so if the
|
|
|
|
// host does not set a scale factor on Windows or Linux we should just use a factor of
|
|
|
|
// 1. That may make the GUI tiny but it also prevents it from getting cut off.
|
|
|
|
#[cfg(target_os = "macos")]
|
2022-03-05 13:37:35 +01:00
|
|
|
scaling_factor: AtomicCell::new(None),
|
2022-04-27 18:33:08 +02:00
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
|
|
scaling_factor: AtomicCell::new(Some(1.0)),
|
2022-02-06 17:12:57 +01:00
|
|
|
}))
|
2022-02-06 00:22:33 +01:00
|
|
|
}
|
|
|
|
|
2022-07-13 23:13:15 +02:00
|
|
|
/// State for an `nih_plug_egui` editor.
|
|
|
|
#[derive(Serialize, Deserialize)]
|
2022-02-08 20:16:39 +01:00
|
|
|
pub struct EguiState {
|
2022-07-13 23:13:15 +02:00
|
|
|
/// The window's size in logical pixels before applying `scale_factor`.
|
2022-10-20 12:26:12 +02:00
|
|
|
#[serde(with = "nih_plug::params::persist::serialize_atomic_cell")]
|
2022-02-08 20:16:39 +01:00
|
|
|
size: AtomicCell<(u32, u32)>,
|
2022-07-13 23:13:15 +02:00
|
|
|
/// Whether the editor's window is currently open.
|
|
|
|
#[serde(skip)]
|
2022-02-08 20:16:39 +01:00
|
|
|
open: AtomicBool,
|
|
|
|
}
|
|
|
|
|
2022-07-13 23:13:15 +02:00
|
|
|
impl<'a> PersistentField<'a, EguiState> for Arc<EguiState> {
|
|
|
|
fn set(&self, new_value: EguiState) {
|
|
|
|
self.size.store(new_value.size.load());
|
|
|
|
}
|
|
|
|
|
|
|
|
fn map<F, R>(&self, f: F) -> R
|
|
|
|
where
|
|
|
|
F: Fn(&EguiState) -> R,
|
|
|
|
{
|
|
|
|
f(self)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-08 20:16:39 +01:00
|
|
|
impl EguiState {
|
2022-03-05 13:37:35 +01:00
|
|
|
/// 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.
|
2022-02-08 20:16:39 +01:00
|
|
|
pub fn from_size(width: u32, height: u32) -> Arc<EguiState> {
|
|
|
|
Arc::new(EguiState {
|
|
|
|
size: AtomicCell::new((width, height)),
|
|
|
|
open: AtomicBool::new(false),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-04-26 19:39:03 +02:00
|
|
|
/// Returns a `(width, height)` pair for the current size of the GUI in logical pixels.
|
2022-02-08 20:16:39 +01:00
|
|
|
pub fn size(&self) -> (u32, u32) {
|
|
|
|
self.size.load()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Whether the GUI is currently visible.
|
2022-02-08 20:33:08 +01:00
|
|
|
// Called `is_open()` instead of `open()` to avoid the ambiguity.
|
|
|
|
pub fn is_open(&self) -> bool {
|
2022-02-08 20:16:39 +01:00
|
|
|
self.open.load(Ordering::Acquire)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-03 23:05:01 +01:00
|
|
|
/// An [`Editor`] implementation that calls an egui draw loop.
|
2022-02-06 17:12:57 +01:00
|
|
|
struct EguiEditor<T> {
|
2022-02-08 20:16:39 +01:00
|
|
|
egui_state: Arc<EguiState>,
|
2022-02-06 17:12:57 +01:00
|
|
|
/// The plugin's state. This is kept in between editor openenings.
|
2022-02-08 20:16:39 +01:00
|
|
|
user_state: Arc<RwLock<T>>,
|
2022-10-20 14:03:55 +02:00
|
|
|
|
|
|
|
/// The user's build function. Applied once at the start of the application.
|
|
|
|
build: Arc<dyn Fn(&Context, &mut T) + 'static + Send + Sync>,
|
2022-03-13 18:03:23 +01:00
|
|
|
/// The user's update function.
|
2022-02-27 16:49:18 +01:00
|
|
|
update: Arc<dyn Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync>,
|
2022-03-05 13:37:35 +01:00
|
|
|
|
|
|
|
/// 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 00:22:33 +01:00
|
|
|
}
|
|
|
|
|
2022-02-06 17:12:57 +01:00
|
|
|
impl<T> Editor for EguiEditor<T>
|
|
|
|
where
|
|
|
|
T: 'static + Send + Sync,
|
|
|
|
{
|
|
|
|
fn spawn(
|
|
|
|
&self,
|
|
|
|
parent: ParentWindowHandle,
|
2022-03-03 23:23:51 +01:00
|
|
|
context: Arc<dyn GuiContext>,
|
2022-10-20 14:41:53 +02:00
|
|
|
) -> Box<dyn std::any::Any + Send> {
|
2022-10-20 14:03:55 +02:00
|
|
|
let build = self.build.clone();
|
2022-02-06 17:12:57 +01:00
|
|
|
let update = self.update.clone();
|
2022-02-08 20:16:39 +01:00
|
|
|
let state = self.user_state.clone();
|
2022-02-06 17:12:57 +01:00
|
|
|
|
2022-03-05 13:37:35 +01:00
|
|
|
let (unscaled_width, unscaled_height) = self.egui_state.size();
|
|
|
|
let scaling_factor = self.scaling_factor.load();
|
2022-02-06 17:12:57 +01:00
|
|
|
let window = EguiWindow::open_parented(
|
|
|
|
&parent,
|
2022-02-07 21:58:59 +01:00
|
|
|
WindowOpenOptions {
|
|
|
|
title: String::from("egui window"),
|
2022-03-05 13:37:35 +01:00
|
|
|
// 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),
|
2022-03-12 23:15:33 +01:00
|
|
|
|
|
|
|
#[cfg(feature = "opengl")]
|
2022-02-07 21:58:59 +01:00
|
|
|
gl_config: Some(GlConfig {
|
2022-02-06 17:12:57 +01:00
|
|
|
version: (3, 2),
|
|
|
|
red_bits: 8,
|
|
|
|
blue_bits: 8,
|
|
|
|
green_bits: 8,
|
2022-02-07 21:58:59 +01:00
|
|
|
alpha_bits: 8,
|
2022-02-06 17:12:57 +01:00
|
|
|
depth_bits: 24,
|
|
|
|
stencil_bits: 8,
|
|
|
|
samples: None,
|
|
|
|
srgb: true,
|
|
|
|
double_buffer: true,
|
|
|
|
vsync: true,
|
|
|
|
..Default::default()
|
2022-02-07 21:58:59 +01:00
|
|
|
}),
|
2022-02-06 17:12:57 +01:00
|
|
|
},
|
|
|
|
state,
|
2022-10-20 14:03:55 +02:00
|
|
|
move |egui_ctx, _queue, state| build(egui_ctx, &mut state.write()),
|
2022-09-22 17:08:17 +02:00
|
|
|
move |egui_ctx, _queue, state| {
|
2022-02-06 17:12:57 +01:00
|
|
|
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.
|
2022-09-22 17:08:17 +02:00
|
|
|
egui_ctx.request_repaint();
|
2022-02-06 17:12:57 +01:00
|
|
|
(update)(egui_ctx, &setter, &mut state.write());
|
|
|
|
},
|
2022-09-22 15:34:59 +02:00
|
|
|
);
|
2022-02-06 17:12:57 +01:00
|
|
|
|
2022-02-08 20:16:39 +01:00
|
|
|
self.egui_state.open.store(true, Ordering::Release);
|
|
|
|
Box::new(EguiEditorHandle {
|
|
|
|
egui_state: self.egui_state.clone(),
|
|
|
|
window,
|
|
|
|
})
|
2022-02-06 17:12:57 +01:00
|
|
|
}
|
2022-02-06 00:22:33 +01:00
|
|
|
|
|
|
|
fn size(&self) -> (u32, u32) {
|
2022-02-08 20:16:39 +01:00
|
|
|
self.egui_state.size()
|
2022-02-06 00:22:33 +01:00
|
|
|
}
|
2022-03-05 13:37:35 +01:00
|
|
|
|
|
|
|
fn set_scale_factor(&self, factor: f32) -> bool {
|
|
|
|
self.scaling_factor.store(Some(factor));
|
|
|
|
true
|
|
|
|
}
|
2022-03-13 18:03:23 +01:00
|
|
|
|
|
|
|
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<AtomicBool>` and only force a redraw when
|
|
|
|
// that boolean is set.
|
|
|
|
}
|
2022-02-06 00:22:33 +01:00
|
|
|
}
|
2022-02-06 15:08:57 +01:00
|
|
|
|
2022-03-03 23:05:01 +01:00
|
|
|
/// The window handle used for [`EguiEditor`].
|
2022-02-06 17:12:57 +01:00
|
|
|
struct EguiEditorHandle {
|
2022-02-08 20:16:39 +01:00
|
|
|
egui_state: Arc<EguiState>,
|
2022-02-06 17:12:57 +01:00
|
|
|
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 {
|
2022-02-06 15:08:57 +01:00
|
|
|
fn drop(&mut self) {
|
2022-02-08 20:16:39 +01:00
|
|
|
self.egui_state.open.store(false, Ordering::Release);
|
2022-02-06 15:08:57 +01:00
|
|
|
// XXX: This should automatically happen when the handle gets dropped, but apparently not
|
|
|
|
self.window.close();
|
|
|
|
}
|
|
|
|
}
|