Files
nih-plug/nih_plug_vizia/src/editor.rs
Robbert van der Helm 99223ed1c8 Prevent HiDPI scale changes after opening editor
Ableton Live does this and it caused mysterious issues where the window
could suddenly be larger than the window's contents when resizing on a
HiDPI setup.
2023-02-27 01:02:52 +01:00

173 lines
6.7 KiB
Rust

//! The [`Editor`] trait implementation for Vizia editors.
use baseview::{WindowHandle, WindowScalePolicy};
use crossbeam::atomic::AtomicCell;
use nih_plug::prelude::{Editor, GuiContext, ParentWindowHandle};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use vizia::context::backend::TextConfig;
use vizia::prelude::*;
use crate::widgets::RawParamEvent;
use crate::{assets, widgets, ViziaState, ViziaTheming};
/// An [`Editor`] implementation that calls an vizia draw loop.
pub(crate) struct ViziaEditor {
pub(crate) vizia_state: Arc<ViziaState>,
/// The user's app function.
pub(crate) app: Arc<dyn Fn(&mut Context, Arc<dyn GuiContext>) + 'static + Send + Sync>,
/// What level of theming to apply. See [`ViziaEditorTheming`].
pub(crate) theming: ViziaTheming,
/// 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<Option<f32>>,
/// Whether to emit a parameters changed event during the next idle callback. This is set in the
/// `parameter_values_changed()` implementation and it can be used by widgets to explicitly
/// check for new parameter values. This is useful when the parameter value is (indirectly) used
/// to compute a property in an event handler. Like when positioning an element based on the
/// display value's width.
pub(crate) emit_parameters_changed_event: Arc<AtomicBool>,
}
impl Editor for ViziaEditor {
fn spawn(
&self,
parent: ParentWindowHandle,
context: Arc<dyn GuiContext>,
) -> Box<dyn std::any::Any + Send> {
let app = self.app.clone();
let vizia_state = self.vizia_state.clone();
let theming = self.theming;
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();
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);
}
// 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())
})
.with_scale_policy(
system_scaling_factor
.map(|factor| WindowScalePolicy::ScaleFactor(factor as f64))
.unwrap_or(WindowScalePolicy::SystemScaleFactor),
)
.inner_size((unscaled_width, unscaled_height))
.user_scale_factor(user_scale_factor)
.with_text_config(TextConfig {
hint: false,
subpixel: true,
})
.on_idle({
let emit_parameters_changed_event = self.emit_parameters_changed_event.clone();
move |cx| {
if emit_parameters_changed_event
.compare_exchange(true, false, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
cx.emit_custom(
Event::new(RawParamEvent::ParametersChanged)
.propagate(Propagation::Subtree),
);
}
}
});
// 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);
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()
}
fn set_scale_factor(&self, factor: f32) -> bool {
// If the editor is currently open then the host must not change the current HiDPI scale as
// we don't have a way to handle that. Ableton Live does this.
if self.vizia_state.is_open() {
return false;
}
// 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
self.scaling_factor.store(Some(factor));
true
}
fn param_value_changed(&self, _id: &str, _normalized_value: f32) {
// This will cause a future idle callback to send a parameters changed event.
// NOTE: We could add an event containing the parameter's ID and the normalized value, but
// these events aren't really necessary for Vizia.
self.emit_parameters_changed_event
.store(true, Ordering::Relaxed);
}
fn param_modulation_changed(&self, _id: &str, _modulation_offset: f32) {
self.emit_parameters_changed_event
.store(true, Ordering::Relaxed);
}
fn param_values_changed(&self) {
self.emit_parameters_changed_event
.store(true, Ordering::Relaxed);
}
}
/// 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();
}
}