diff --git a/nih_plug_egui/src/lib.rs b/nih_plug_egui/src/lib.rs index acb89636..1ee28c2d 100644 --- a/nih_plug_egui/src/lib.rs +++ b/nih_plug_egui/src/lib.rs @@ -27,8 +27,6 @@ pub mod widgets; /// field on your parameters struct. /// /// See [`EguiState::from_size()`]. -// -// TODO: DPI scaling, this needs to be implemented on the framework level pub fn create_egui_editor( egui_state: Arc, user_state: T, @@ -42,6 +40,8 @@ where egui_state, user_state: Arc::new(RwLock::new(user_state)), update: Arc::new(update), + + scaling_factor: AtomicCell::new(None), })) } @@ -53,7 +53,8 @@ pub struct 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 { Arc::new(EguiState { 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) { self.size.load() } @@ -79,6 +80,10 @@ struct EguiEditor { /// The plugin's state. This is kept in between editor openenings. user_state: Arc>, update: Arc, + + /// 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>, } impl Editor for EguiEditor @@ -93,16 +98,19 @@ where let update = self.update.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( &parent, WindowOpenOptions { title: String::from("egui window"), - size: Size::new(width as f64, height as f64), - // TODO: Implement the plugin-specific DPI scaling APIs with a method on the - // `GuiContext` when baseview gets window resizing. For some reason passing - // 1.0 here causes the UI to be scaled on macOS but not the mouse events. - scale: WindowScalePolicy::SystemScaleFactor, + // 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, @@ -144,6 +152,11 @@ where fn size(&self) -> (u32, u32) { 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`]. diff --git a/src/plugin.rs b/src/plugin.rs index 14dbf03e..1ef0ff50 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -201,6 +201,9 @@ pub trait Editor: Send + Sync { /// gets closed. Implement the [`Drop`] trait on the returned handle if you need to explicitly /// 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 /// called again. // @@ -216,14 +219,24 @@ pub trait Editor: Send + Sync { context: Arc, ) -> Box; - /// 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); + /// 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 // 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 // itself. This would also need an associated `PREFERRED_FRAME_RATE` constant. - // TODO: Add the things needed for DPI scaling // TODO: Resizing } diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index 9988ba1a..db0fbb0f 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -2,6 +2,7 @@ // explicitly pattern match on that unit #![allow(clippy::unused_unit)] +use atomic_float::AtomicF32; use atomic_refcell::{AtomicRefCell, AtomicRefMut}; use clap_sys::events::{ clap_event_header, clap_event_note, clap_event_param_mod, clap_event_param_value, @@ -101,6 +102,11 @@ pub struct Wrapper { /// A handle for the currently active editor instance. The plugin should implement `Drop` on /// this handle for its closing behavior. editor_handle: RwLock>>, + /// 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, /// The current IO configuration, modified through the `clap_plugin_audio_ports_config` @@ -327,6 +333,7 @@ impl Wrapper

{ plugin, editor, editor_handle: RwLock::new(None), + editor_scaling_factor: AtomicF32::new(1.0), is_processing: AtomicBool::new(false), current_bus_config: AtomicCell::new(BusConfig { @@ -1205,9 +1212,29 @@ impl Wrapper

{ } } - unsafe extern "C" fn ext_gui_set_scale(_plugin: *const clap_plugin, _scale: f64) -> bool { - // TOOD: Implement DPI scaling - false + unsafe extern "C" fn ext_gui_set_scale(plugin: *const clap_plugin, scale: f64) -> bool { + check_null_ptr!(false, plugin); + 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( @@ -1218,15 +1245,14 @@ impl Wrapper

{ check_null_ptr!(false, plugin, width, height); let wrapper = &*(plugin as *const Self); - match &wrapper.editor { - Some(editor) => { - (*width, *height) = editor.size(); - true - } - None => { - unreachable!("We don't return the editor extension on plugins without an editor"); - } - } + let (unscaled_width, unscaled_height) = wrapper.editor.as_ref().unwrap().size(); + let scaling_factor = wrapper.editor_scaling_factor.load(Ordering::Relaxed); + (*width, *height) = ( + (unscaled_width as f32 * scaling_factor).round() as u32, + (unscaled_height as f32 * scaling_factor).round() as u32, + ); + + true } unsafe extern "C" fn ext_gui_can_resize(_plugin: *const clap_plugin) -> bool { @@ -1251,15 +1277,14 @@ impl Wrapper

{ check_null_ptr!(false, plugin); let wrapper = &*(plugin as *const Self); - match &wrapper.editor { - Some(editor) => { - let (editor_width, editor_height) = editor.size(); - width == editor_width && height == editor_height - } - None => { - unreachable!("We don't return the editor extension on plugins without an editor"); - } - } + let (unscaled_width, unscaled_height) = wrapper.editor.as_ref().unwrap().size(); + let scaling_factor = wrapper.editor_scaling_factor.load(Ordering::Relaxed); + let (editor_width, editor_height) = ( + (unscaled_width as f32 * scaling_factor).round() as u32, + (unscaled_height as f32 * scaling_factor).round() as u32, + ); + + width == editor_width && height == editor_height } unsafe extern "C" fn ext_gui_show(_plugin: *const clap_plugin) { diff --git a/src/wrapper/vst3/view.rs b/src/wrapper/vst3/view.rs index d8b82c78..48628d90 100644 --- a/src/wrapper/vst3/view.rs +++ b/src/wrapper/vst3/view.rs @@ -1,12 +1,14 @@ +use atomic_float::AtomicF32; use parking_lot::RwLock; use raw_window_handle::RawWindowHandle; use std::any::Any; use std::ffi::{c_void, CStr}; use std::mem; +use std::sync::atomic::Ordering; use std::sync::Arc; use vst3_com::utils::SharedVstPtr; 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 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 /// editor. This is managed separately so the lifetime bounds match up. -#[VST3(implements(IPlugView))] +#[VST3(implements(IPlugView, IPlugViewContentScaleSupport))] pub(crate) struct WrapperView { inner: Arc>, editor: Arc, editor_handle: RwLock>>, - /// 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>>, + /// 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 WrapperView

{ pub fn new(inner: Arc>, editor: Arc) -> Box { - 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 IPlugView for WrapperView

{ *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; size.left = 0; - size.right = width as i32; + size.right = (unscaled_width as f32 * scaling_factor).round() as i32; size.top = 0; - size.bottom = height as i32; + size.bottom = (unscaled_height as f32 * scaling_factor).round() as i32; 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 - 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 { @@ -217,3 +246,20 @@ impl IPlugView for WrapperView

{ } } } + +impl IPlugViewContentScaleSupport for WrapperView

{ + 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 + } + } +}