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

273 lines
10 KiB
Rust
Raw Normal View History

2022-03-18 09:14:45 +11:00
//! [VIZIA](https://github.com/vizia/vizia) editor support for NIH plug.
// See the comment in the main `nih_plug` crate
#![allow(clippy::type_complexity)]
2022-03-18 09:14:45 +11:00
use baseview::{WindowHandle, WindowScalePolicy};
use crossbeam::atomic::AtomicCell;
use nih_plug::params::persist::PersistentField;
use nih_plug::prelude::{Editor, GuiContext, ParentWindowHandle};
use serde::{Deserialize, Serialize};
2022-03-18 09:14:45 +11:00
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use vizia::prelude::*;
2022-03-18 09:14:45 +11:00
// Re-export for convenience
pub use vizia;
2022-03-18 10:22:58 +11:00
pub mod assets;
2022-03-19 01:26:19 +11:00
pub mod widgets;
2022-03-18 09:14:45 +11:00
/// Create an [`Editor`] instance using a [`vizia`][::vizia] GUI. The [`ViziaState`] 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.
///
/// The [`GuiContext`] is also passed to the app function. This is only meant for saving and
/// restoring state as part of your plugin's preset handling. You should not interact with this
/// directly to set parameters. Use the `ParamEvent`s instead.
///
/// The `theming` argument controls what level of theming to apply. If you use
/// [`ViziaTheming::Custom`], then you **need** to call
/// [`nih_plug_vizia::assets::register_noto_sans_light()`][assets::register_noto_sans_light()] at
/// the start of your app function.
///
2022-03-18 09:14:45 +11:00
/// See [VIZIA](https://github.com/vizia/vizia)'s repository for examples on how to use this.
pub fn create_vizia_editor<F>(
vizia_state: Arc<ViziaState>,
theming: ViziaTheming,
app: F,
) -> Option<Box<dyn Editor>>
2022-03-18 09:14:45 +11:00
where
F: Fn(&mut Context, Arc<dyn GuiContext>) + 'static + Send + Sync,
2022-03-18 09:14:45 +11:00
{
Some(Box::new(ViziaEditor {
vizia_state,
app: Arc::new(app),
theming,
// 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")]
scaling_factor: AtomicCell::new(None),
#[cfg(not(target_os = "macos"))]
scaling_factor: AtomicCell::new(Some(1.0)),
}))
}
/// Controls what level of theming to apply to the editor.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)]
pub enum ViziaTheming {
/// Disable both `nih_plug_vizia`'s and vizia's built-in theming.
None,
/// Disable `nih_plug_vizia`'s custom theming.
Builtin,
/// Apply `nih_plug_vizia`'s custom theming. This is the default. You **need** to call
/// [`nih_plug_vizia::assets::register_noto_sans_light()`][assets::register_noto_sans_light()]
/// at the start of your app function for the font to work correctly.
#[default]
Custom,
2022-03-18 09:14:45 +11:00
}
/// State for an `nih_plug_vizia` editor. The scale factor can be manipulated at runtime by changing
2022-04-12 00:23:34 +10:00
/// `cx.user_scale_factor`.
#[derive(Serialize, Deserialize)]
2022-03-18 09:14:45 +11:00
pub struct ViziaState {
/// The window's size in logical pixels before applying `scale_factor`.
#[serde(with = "nih_plug::params::persist::serialize_atomic_cell")]
2022-03-18 09:14:45 +11:00
size: AtomicCell<(u32, u32)>,
/// A scale factor that should be applied to `size` separate from from any system HiDPI scaling.
/// This can be used to allow GUIs to be scaled uniformly.
#[serde(with = "nih_plug::params::persist::serialize_atomic_cell")]
scale_factor: AtomicCell<f64>,
/// Whether the editor's window is currently open.
#[serde(skip)]
2022-03-18 09:14:45 +11:00
open: AtomicBool,
}
impl<'a> PersistentField<'a, ViziaState> for Arc<ViziaState> {
fn set(&self, new_value: ViziaState) {
self.size.store(new_value.size.load());
self.scale_factor.store(new_value.scale_factor.load());
}
fn map<F, R>(&self, f: F) -> R
where
F: Fn(&ViziaState) -> R,
{
f(self)
}
}
2022-03-18 09:14:45 +11:00
impl ViziaState {
/// Initialize the GUI's state. This value can be passed to [`create_vizia_editor()`]. The
/// window size is in logical pixels, so before it is multiplied by the DPI scaling factor.
2022-03-18 09:14:45 +11:00
pub fn from_size(width: u32, height: u32) -> Arc<ViziaState> {
Arc::new(ViziaState {
size: AtomicCell::new((width, height)),
scale_factor: AtomicCell::new(1.0),
2022-03-18 09:14:45 +11:00
open: AtomicBool::new(false),
})
}
/// The same as [`from_size()`][Self::from_size()], but with a separate initial scale factor.
/// This scale factor gets applied on top of any HiDPI scaling, and it can be modified at
2022-04-12 00:23:34 +10:00
/// runtime by changing `cx.user_scale_factor`.
pub fn from_size_with_scale(width: u32, height: u32, scale_factor: f64) -> Arc<ViziaState> {
Arc::new(ViziaState {
size: AtomicCell::new((width, height)),
scale_factor: AtomicCell::new(scale_factor),
open: AtomicBool::new(false),
})
}
/// Returns a `(width, height)` pair for the current size of the GUI in logical pixels, after
/// applying the user scale factor.
pub fn scaled_logical_size(&self) -> (u32, u32) {
let (logical_width, logical_height) = self.size.load();
let scale_factor = self.scale_factor.load();
(
(logical_width as f64 * scale_factor).round() as u32,
(logical_height as f64 * scale_factor).round() as u32,
)
}
/// Returns a `(width, height)` pair for the current size of the GUI in logical pixels before
/// applying the user scale factor.
pub fn inner_logical_size(&self) -> (u32, u32) {
2022-03-18 09:14:45 +11:00
self.size.load()
}
/// Get the non-DPI related uniform scaling factor the GUI's size will be multiplied with. This
2022-04-12 00:23:34 +10:00
/// can be changed by changing `cx.user_scale_factor`.
pub fn user_scale_factor(&self) -> f64 {
self.scale_factor.load()
}
2022-03-18 09:14:45 +11:00
/// 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 vizia draw loop.
struct ViziaEditor {
vizia_state: Arc<ViziaState>,
/// The user's app function.
app: Arc<dyn Fn(&mut Context, Arc<dyn GuiContext>) + 'static + Send + Sync>,
/// What level of theming to apply. See [`ViziaEditorTheming`].
theming: ViziaTheming,
2022-03-18 09:14:45 +11: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>>,
}
impl Editor for ViziaEditor {
fn spawn(
&self,
parent: ParentWindowHandle,
context: Arc<dyn GuiContext>,
) -> Box<dyn std::any::Any + Send> {
2022-03-18 09:14:45 +11:00
let app = self.app.clone();
let vizia_state = self.vizia_state.clone();
let theming = self.theming;
2022-03-18 09:14:45 +11:00
let (unscaled_width, unscaled_height) = vizia_state.inner_logical_size();
let system_scaling_factor = self.scaling_factor.load();
let user_scale_factor = vizia_state.user_scale_factor();
2022-03-18 09:14:45 +11:00
let mut application = Application::new(move |cx| {
// Set some default styles to match the iced integration
if theming >= ViziaTheming::Custom {
// NOTE: vizia's font rendering looks way too dark and thick. Going one font weight
// lower seems to compensate for this.
cx.set_default_font(assets::NOTO_SANS_LIGHT);
cx.add_theme(include_str!("../assets/theme.css"));
// There doesn't seem to be any way to bundle styles with a widget, so we'll always
// include the style sheet for our custom widgets at context creation
widgets::register_theme(cx);
}
2022-03-19 01:26:19 +11:00
// Any widget can change the parameters by emitting `ParamEvent` events. This model will
// handle them automatically.
widgets::ParamModel {
context: context.clone(),
}
.build(cx);
// And we'll link `WindowEvent::ResizeWindow` and `WindowEvent::SetScale` events to our
// `ViziaState`. We'll notify the host when any of these change.
widgets::WindowModel {
context: context.clone(),
vizia_state: vizia_state.clone(),
}
.build(cx);
app(cx, context.clone())
2022-03-18 09:14:45 +11:00
})
.with_scale_policy(
system_scaling_factor
2022-03-18 09:14:45 +11:00
.map(|factor| WindowScalePolicy::ScaleFactor(factor as f64))
.unwrap_or(WindowScalePolicy::SystemScaleFactor),
)
2022-04-26 22:45:06 +10:00
.inner_size((unscaled_width, unscaled_height))
.user_scale_factor(user_scale_factor);
// This way the plugin can decide to use none of the built in theming
if theming == ViziaTheming::None {
application = application.ignore_default_theme();
}
let window = application.open_parented(&parent);
2022-03-18 09:14:45 +11:00
self.vizia_state.open.store(true, Ordering::Release);
Box::new(ViziaEditorHandle {
vizia_state: self.vizia_state.clone(),
window,
})
}
fn size(&self) -> (u32, u32) {
// This includes the user scale factor if set, but not any HiDPI scaling
self.vizia_state.scaled_logical_size()
2022-03-18 09:14:45 +11:00
}
fn set_scale_factor(&self, factor: f32) -> bool {
// We're making things a bit more complicated by having both a system scale factor, which is
// used for HiDPI and also known to the host, and a user scale factor that the user can use
// to arbitrarily resize the GUI
2022-03-18 09:14:45 +11:00
self.scaling_factor.store(Some(factor));
true
}
fn param_values_changed(&self) {
// TODO: Update the GUI when this happens, right now this happens automatically as a result
// of of the reactivity
2022-03-18 09:14:45 +11:00
}
}
/// The window handle used for [`ViziaEditor`].
struct ViziaEditorHandle {
vizia_state: Arc<ViziaState>,
window: WindowHandle,
}
/// The window handle enum stored within 'WindowHandle' contains raw pointers. Is there a way around
/// having this requirement?
unsafe impl Send for ViziaEditorHandle {}
impl Drop for ViziaEditorHandle {
fn drop(&mut self) {
self.vizia_state.open.store(false, Ordering::Release);
// XXX: This should automatically happen when the handle gets dropped, but apparently not
self.window.close();
}
}