1
0
Fork 0

Add DPI scaling support

That hopefully works.
This commit is contained in:
Robbert van der Helm 2022-03-05 13:37:35 +01:00
parent e491ff6319
commit 9267a8371c
4 changed files with 139 additions and 42 deletions

View file

@ -27,8 +27,6 @@ pub mod widgets;
/// field on your parameters struct. /// field on your parameters struct.
/// ///
/// See [`EguiState::from_size()`]. /// See [`EguiState::from_size()`].
//
// TODO: DPI scaling, this needs to be implemented on the framework level
pub fn create_egui_editor<T, U>( pub fn create_egui_editor<T, U>(
egui_state: Arc<EguiState>, egui_state: Arc<EguiState>,
user_state: T, user_state: T,
@ -42,6 +40,8 @@ where
egui_state, egui_state,
user_state: Arc::new(RwLock::new(user_state)), user_state: Arc::new(RwLock::new(user_state)),
update: Arc::new(update), update: Arc::new(update),
scaling_factor: AtomicCell::new(None),
})) }))
} }
@ -53,7 +53,8 @@ pub struct EguiState {
} }
impl EguiState { impl EguiState {
/// Initialize the GUI's state. This is passed to [`create_egui_editor()`]. /// 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> { pub fn from_size(width: u32, height: u32) -> Arc<EguiState> {
Arc::new(EguiState { Arc::new(EguiState {
size: AtomicCell::new((width, height)), size: AtomicCell::new((width, height)),
@ -61,7 +62,7 @@ impl EguiState {
}) })
} }
/// Return a `(width, height)` pair for the current size of the GUI. /// Return a `(width, height)` pair for the current size of the GUI in logical pixels.
pub fn size(&self) -> (u32, u32) { pub fn size(&self) -> (u32, u32) {
self.size.load() self.size.load()
} }
@ -79,6 +80,10 @@ struct EguiEditor<T> {
/// The plugin's state. This is kept in between editor openenings. /// The plugin's state. This is kept in between editor openenings.
user_state: Arc<RwLock<T>>, user_state: Arc<RwLock<T>>,
update: Arc<dyn Fn(&Context, &ParamSetter, &mut T) + 'static + Send + Sync>, 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>>,
} }
impl<T> Editor for EguiEditor<T> impl<T> Editor for EguiEditor<T>
@ -93,16 +98,19 @@ where
let update = self.update.clone(); let update = self.update.clone();
let state = self.user_state.clone(); let state = self.user_state.clone();
let (width, height) = self.egui_state.size(); let (unscaled_width, unscaled_height) = self.egui_state.size();
let scaling_factor = self.scaling_factor.load();
let window = EguiWindow::open_parented( let window = EguiWindow::open_parented(
&parent, &parent,
WindowOpenOptions { WindowOpenOptions {
title: String::from("egui window"), title: String::from("egui window"),
size: Size::new(width as f64, height as f64), // Baseview should be doing the DPI scaling for us
// TODO: Implement the plugin-specific DPI scaling APIs with a method on the size: Size::new(unscaled_width as f64, unscaled_height as f64),
// `GuiContext` when baseview gets window resizing. For some reason passing // NOTE: For some reason passing 1.0 here causes the UI to be scaled on macOS but
// 1.0 here causes the UI to be scaled on macOS but not the mouse events. // not the mouse events.
scale: WindowScalePolicy::SystemScaleFactor, scale: scaling_factor
.map(|factor| WindowScalePolicy::ScaleFactor(factor as f64))
.unwrap_or(WindowScalePolicy::SystemScaleFactor),
gl_config: Some(GlConfig { gl_config: Some(GlConfig {
version: (3, 2), version: (3, 2),
red_bits: 8, red_bits: 8,
@ -144,6 +152,11 @@ where
fn size(&self) -> (u32, u32) { fn size(&self) -> (u32, u32) {
self.egui_state.size() self.egui_state.size()
} }
fn set_scale_factor(&self, factor: f32) -> bool {
self.scaling_factor.store(Some(factor));
true
}
} }
/// The window handle used for [`EguiEditor`]. /// The window handle used for [`EguiEditor`].

View file

@ -201,6 +201,9 @@ pub trait Editor: Send + Sync {
/// gets closed. Implement the [`Drop`] trait on the returned handle if you need to explicitly /// gets closed. Implement the [`Drop`] trait on the returned handle if you need to explicitly
/// handle the editor's closing behavior. /// handle the editor's closing behavior.
/// ///
/// If [`set_scale_factor()`][Self::set_scale_factor()] has been called, then any created
/// windows should have their sizes multiplied by that factor.
///
/// The wrapper guarantees that a previous handle has been dropped before this function is /// The wrapper guarantees that a previous handle has been dropped before this function is
/// called again. /// called again.
// //
@ -216,14 +219,24 @@ pub trait Editor: Send + Sync {
context: Arc<dyn GuiContext>, context: Arc<dyn GuiContext>,
) -> Box<dyn Any + Send + Sync>; ) -> Box<dyn Any + Send + Sync>;
/// Return the (currnent) size of the editor in pixels as a `(width, height)` pair. /// Return the (currnent) size of the editor in pixels as a `(width, height)` pair. This size
/// must be reported in _logical pixels_, i.e. the size before being multiplied by the DPI
/// scaling factor to get the actual physical screen pixels.
fn size(&self) -> (u32, u32); fn size(&self) -> (u32, u32);
/// Set the DPI scaling factor, if supported. The plugin APIs don't make any guarantees on when
/// this is called, but for now just assume it will be the first function that gets called
/// before creating the editor. If this is set, then any windows created by this editor should
/// have their sizes multiplied by this scaling factor on Windows and Linux.
///
/// Right now this is never called on macOS since DPI scaling is built into the operating system
/// there.
fn set_scale_factor(&self, factor: f32) -> bool;
// TODO: Reconsider adding a tick function here for the Linux `IRunLoop`. To keep this platform // TODO: Reconsider adding a tick function here for the Linux `IRunLoop`. To keep this platform
// and API agnostic, add a way to ask the GuiContext if the wrapper already provides a // and API agnostic, add a way to ask the GuiContext if the wrapper already provides a
// tick function. If it does not, then the Editor implementation must handle this by // tick function. If it does not, then the Editor implementation must handle this by
// itself. This would also need an associated `PREFERRED_FRAME_RATE` constant. // itself. This would also need an associated `PREFERRED_FRAME_RATE` constant.
// TODO: Add the things needed for DPI scaling
// TODO: Resizing // TODO: Resizing
} }

View file

@ -2,6 +2,7 @@
// explicitly pattern match on that unit // explicitly pattern match on that unit
#![allow(clippy::unused_unit)] #![allow(clippy::unused_unit)]
use atomic_float::AtomicF32;
use atomic_refcell::{AtomicRefCell, AtomicRefMut}; use atomic_refcell::{AtomicRefCell, AtomicRefMut};
use clap_sys::events::{ use clap_sys::events::{
clap_event_header, clap_event_note, clap_event_param_mod, clap_event_param_value, clap_event_header, clap_event_note, clap_event_param_mod, clap_event_param_value,
@ -101,6 +102,11 @@ pub struct Wrapper<P: ClapPlugin> {
/// A handle for the currently active editor instance. The plugin should implement `Drop` on /// A handle for the currently active editor instance. The plugin should implement `Drop` on
/// this handle for its closing behavior. /// this handle for its closing behavior.
editor_handle: RwLock<Option<Box<dyn Any + Send + Sync>>>, editor_handle: RwLock<Option<Box<dyn Any + Send + Sync>>>,
/// The DPI scaling factor as passed to the [IPlugViewContentScaleSupport::set_scale_factor()]
/// function. Defaults to 1.0, and will be kept there on macOS. When reporting and handling size
/// the sizes communicated to and from the DAW should be scaled by this factor since NIH-plug's
/// APIs only deal in logical pixels.
editor_scaling_factor: AtomicF32,
is_processing: AtomicBool, is_processing: AtomicBool,
/// The current IO configuration, modified through the `clap_plugin_audio_ports_config` /// The current IO configuration, modified through the `clap_plugin_audio_ports_config`
@ -327,6 +333,7 @@ impl<P: ClapPlugin> Wrapper<P> {
plugin, plugin,
editor, editor,
editor_handle: RwLock::new(None), editor_handle: RwLock::new(None),
editor_scaling_factor: AtomicF32::new(1.0),
is_processing: AtomicBool::new(false), is_processing: AtomicBool::new(false),
current_bus_config: AtomicCell::new(BusConfig { current_bus_config: AtomicCell::new(BusConfig {
@ -1205,9 +1212,29 @@ impl<P: ClapPlugin> Wrapper<P> {
} }
} }
unsafe extern "C" fn ext_gui_set_scale(_plugin: *const clap_plugin, _scale: f64) -> bool { unsafe extern "C" fn ext_gui_set_scale(plugin: *const clap_plugin, scale: f64) -> bool {
// TOOD: Implement DPI scaling check_null_ptr!(false, plugin);
false let wrapper = &*(plugin as *const Self);
// On macOS scaling is done by the OS, and all window sizes are in logical pixels
if cfg!(target_os = "macos") {
nih_debug_assert_failure!("Ignoring host request to set explicit DPI scaling factor");
return false;
}
if wrapper
.editor
.as_ref()
.unwrap()
.set_scale_factor(scale as f32)
{
wrapper
.editor_scaling_factor
.store(scale as f32, std::sync::atomic::Ordering::Relaxed);
true
} else {
false
}
} }
unsafe extern "C" fn ext_gui_get_size( unsafe extern "C" fn ext_gui_get_size(
@ -1218,15 +1245,14 @@ impl<P: ClapPlugin> Wrapper<P> {
check_null_ptr!(false, plugin, width, height); check_null_ptr!(false, plugin, width, height);
let wrapper = &*(plugin as *const Self); let wrapper = &*(plugin as *const Self);
match &wrapper.editor { let (unscaled_width, unscaled_height) = wrapper.editor.as_ref().unwrap().size();
Some(editor) => { let scaling_factor = wrapper.editor_scaling_factor.load(Ordering::Relaxed);
(*width, *height) = editor.size(); (*width, *height) = (
true (unscaled_width as f32 * scaling_factor).round() as u32,
} (unscaled_height as f32 * scaling_factor).round() as u32,
None => { );
unreachable!("We don't return the editor extension on plugins without an editor");
} true
}
} }
unsafe extern "C" fn ext_gui_can_resize(_plugin: *const clap_plugin) -> bool { unsafe extern "C" fn ext_gui_can_resize(_plugin: *const clap_plugin) -> bool {
@ -1251,15 +1277,14 @@ impl<P: ClapPlugin> Wrapper<P> {
check_null_ptr!(false, plugin); check_null_ptr!(false, plugin);
let wrapper = &*(plugin as *const Self); let wrapper = &*(plugin as *const Self);
match &wrapper.editor { let (unscaled_width, unscaled_height) = wrapper.editor.as_ref().unwrap().size();
Some(editor) => { let scaling_factor = wrapper.editor_scaling_factor.load(Ordering::Relaxed);
let (editor_width, editor_height) = editor.size(); let (editor_width, editor_height) = (
width == editor_width && height == editor_height (unscaled_width as f32 * scaling_factor).round() as u32,
} (unscaled_height as f32 * scaling_factor).round() as u32,
None => { );
unreachable!("We don't return the editor extension on plugins without an editor");
} width == editor_width && height == editor_height
}
} }
unsafe extern "C" fn ext_gui_show(_plugin: *const clap_plugin) { unsafe extern "C" fn ext_gui_show(_plugin: *const clap_plugin) {

View file

@ -1,12 +1,14 @@
use atomic_float::AtomicF32;
use parking_lot::RwLock; use parking_lot::RwLock;
use raw_window_handle::RawWindowHandle; use raw_window_handle::RawWindowHandle;
use std::any::Any; use std::any::Any;
use std::ffi::{c_void, CStr}; use std::ffi::{c_void, CStr};
use std::mem; use std::mem;
use std::sync::atomic::Ordering;
use std::sync::Arc; use std::sync::Arc;
use vst3_com::utils::SharedVstPtr; use vst3_com::utils::SharedVstPtr;
use vst3_sys::base::{kInvalidArgument, kResultFalse, kResultOk, tresult, TBool}; use vst3_sys::base::{kInvalidArgument, kResultFalse, kResultOk, tresult, TBool};
use vst3_sys::gui::{IPlugFrame, IPlugView}; use vst3_sys::gui::{IPlugFrame, IPlugView, IPlugViewContentScaleSupport};
use vst3_sys::VST3; use vst3_sys::VST3;
use super::inner::WrapperInner; use super::inner::WrapperInner;
@ -30,19 +32,30 @@ const VST3_PLATFORM_X11_WINDOW: &str = "X11EmbedWindowID";
/// The plugin's [`IPlugView`] instance created in [`IEditController::create_view()`] if `P` has an /// The plugin's [`IPlugView`] instance created in [`IEditController::create_view()`] if `P` has an
/// editor. This is managed separately so the lifetime bounds match up. /// editor. This is managed separately so the lifetime bounds match up.
#[VST3(implements(IPlugView))] #[VST3(implements(IPlugView, IPlugViewContentScaleSupport))]
pub(crate) struct WrapperView<P: Vst3Plugin> { pub(crate) struct WrapperView<P: Vst3Plugin> {
inner: Arc<WrapperInner<P>>, inner: Arc<WrapperInner<P>>,
editor: Arc<dyn Editor>, editor: Arc<dyn Editor>,
editor_handle: RwLock<Option<Box<dyn Any>>>, editor_handle: RwLock<Option<Box<dyn Any>>>,
/// The `IPlugFrame` instance passed by the host during `IPlugView::set_frame`. /// The `IPlugFrame` instance passed by the host during [IPlugView::set_frame()].
pub plug_frame: RwLock<Option<VstPtr<dyn IPlugFrame>>>, pub plug_frame: RwLock<Option<VstPtr<dyn IPlugFrame>>>,
/// The DPI scaling factor as passed to the [IPlugViewContentScaleSupport::set_scale_factor()]
/// function. Defaults to 1.0, and will be kept there on macOS. When reporting and handling size
/// the sizes communicated to and from the DAW should be scaled by this factor since NIH-plug's
/// APIs only deal in logical pixels.
scaling_factor: AtomicF32,
} }
impl<P: Vst3Plugin> WrapperView<P> { impl<P: Vst3Plugin> WrapperView<P> {
pub fn new(inner: Arc<WrapperInner<P>>, editor: Arc<dyn Editor>) -> Box<Self> { pub fn new(inner: Arc<WrapperInner<P>>, editor: Arc<dyn Editor>) -> Box<Self> {
Self::allocate(inner, editor, RwLock::new(None), RwLock::new(None)) Self::allocate(
inner,
editor,
RwLock::new(None),
RwLock::new(None),
AtomicF32::new(1.0),
)
} }
} }
@ -171,19 +184,35 @@ impl<P: Vst3Plugin> IPlugView for WrapperView<P> {
*size = mem::zeroed(); *size = mem::zeroed();
let (width, height) = self.editor.size(); let (unscaled_width, unscaled_height) = self.editor.size();
let scaling_factor = self.scaling_factor.load(Ordering::Relaxed);
let size = &mut *size; let size = &mut *size;
size.left = 0; size.left = 0;
size.right = width as i32; size.right = (unscaled_width as f32 * scaling_factor).round() as i32;
size.top = 0; size.top = 0;
size.bottom = height as i32; size.bottom = (unscaled_height as f32 * scaling_factor).round() as i32;
kResultOk kResultOk
} }
unsafe fn on_size(&self, _new_size: *mut vst3_sys::gui::ViewRect) -> tresult { unsafe fn on_size(&self, new_size: *mut vst3_sys::gui::ViewRect) -> tresult {
// TODO: Implement resizing // TODO: Implement resizing
kResultOk check_null_ptr!(new_size);
let (unscaled_width, unscaled_height) = self.editor.size();
let scaling_factor = self.scaling_factor.load(Ordering::Relaxed);
let (editor_width, editor_height) = (
(unscaled_width as f32 * scaling_factor).round() as i32,
(unscaled_height as f32 * scaling_factor).round() as i32,
);
let width = (*new_size).right - (*new_size).left;
let height = (*new_size).bottom - (*new_size).top;
if width == editor_width && height == editor_height {
kResultOk
} else {
kResultFalse
}
} }
unsafe fn on_focus(&self, _state: TBool) -> tresult { unsafe fn on_focus(&self, _state: TBool) -> tresult {
@ -217,3 +246,20 @@ impl<P: Vst3Plugin> IPlugView for WrapperView<P> {
} }
} }
} }
impl<P: Vst3Plugin> IPlugViewContentScaleSupport for WrapperView<P> {
unsafe fn set_scale_factor(&self, factor: f32) -> tresult {
// On macOS scaling is done by the OS, and all window sizes are in logical pixels
if cfg!(target_os = "macos") {
nih_debug_assert_failure!("Ignoring host request to set explicit DPI scaling factor");
return kResultFalse;
}
if self.editor.set_scale_factor(factor) {
self.scaling_factor.store(factor, Ordering::Relaxed);
kResultOk
} else {
kResultFalse
}
}
}