From 1fa2b5f74fb5a9983c33060caea221254222e9d2 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 13 Mar 2022 18:03:23 +0100 Subject: [PATCH] Add an Editor callback to notify parameter changes This will be needed to allow iced to be reactive in our model. --- Cargo.lock | 2 +- nih_plug_egui/src/lib.rs | 7 ++ nih_plug_iced/src/lib.rs | 91 +++++++------------- nih_plug_iced/src/wrapper.rs | 158 +++++++++++++++++++++++++++++++++++ src/plugin.rs | 9 ++ 5 files changed, 207 insertions(+), 60 deletions(-) create mode 100644 nih_plug_iced/src/wrapper.rs diff --git a/Cargo.lock b/Cargo.lock index c8591cfb..8f438832 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1396,7 +1396,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "iced_baseview" version = "0.0.3" -source = "git+https://github.com/robbert-vdh/iced_baseview.git?branch=feature/update-dependencies#61af92e930ce6500ab3a8321ed29a07c26a9e396" +source = "git+https://github.com/robbert-vdh/iced_baseview.git?branch=feature/update-dependencies#d6356218bc43a4b74e279cdd364de108cb7fa269" dependencies = [ "baseview", "copypasta", diff --git a/nih_plug_egui/src/lib.rs b/nih_plug_egui/src/lib.rs index 4ade9c16..842085bf 100644 --- a/nih_plug_egui/src/lib.rs +++ b/nih_plug_egui/src/lib.rs @@ -82,6 +82,7 @@ struct EguiEditor { egui_state: Arc, /// The plugin's state. This is kept in between editor openenings. user_state: Arc>, + /// The user's update function. update: Arc, /// The scaling factor reported by the host, if any. On macOS this will never be set and we @@ -162,6 +163,12 @@ where self.scaling_factor.store(Some(factor)); true } + + 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` and only force a redraw when + // that boolean is set. + } } /// The window handle used for [`EguiEditor`]. diff --git a/nih_plug_iced/src/lib.rs b/nih_plug_iced/src/lib.rs index e8c37738..8955454d 100644 --- a/nih_plug_iced/src/lib.rs +++ b/nih_plug_iced/src/lib.rs @@ -4,6 +4,7 @@ use baseview::{Size, WindowOpenOptions, WindowScalePolicy}; use crossbeam::atomic::AtomicCell; +use crossbeam::channel; use nih_plug::prelude::{Editor, GuiContext, ParentWindowHandle}; use std::fmt::Debug; use std::sync::atomic::{AtomicBool, Ordering}; @@ -15,6 +16,7 @@ use crate::widgets::ParamMessage; pub use iced_baseview::*; pub mod widgets; +mod wrapper; /// Create an [`Editor`] instance using [iced](https://github.com/iced-rs/iced). The rough idea is /// that you implement [`IcedEditor`], which is roughly analogous to iced's regular [`Application`] @@ -30,11 +32,20 @@ pub fn create_iced_editor( iced_state: Arc, initialization_flags: E::InitializationFlags, ) -> Option> { + // We need some way to communicate parameter changes to the `IcedEditor` since parameter updates + // come from outside of the editor's reactive model. This contains only capacity to store only + // one parameter update, since we're only storing _that_ a parameter update has happened and not + // which parameter so we'd need to redraw the entire GUI either way. + let (parameter_updates_sender, parameter_updates_receiver) = channel::bounded(1); + Some(Box::new(IcedEditorWrapper:: { iced_state, initialization_flags, scaling_factor: AtomicCell::new(None), + + parameter_updates_sender, + parameter_updates_receiver: Arc::new(parameter_updates_receiver), })) } @@ -151,6 +162,9 @@ impl IcedState { } } +/// A marker struct to indicate that a parameter update has happened. +pub(crate) struct ParameterUpdate; + /// An [`Editor`] implementation that renders an iced [`Application`]. struct IcedEditorWrapper { iced_state: Arc, @@ -159,6 +173,10 @@ struct IcedEditorWrapper { /// 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>, + + /// A subscription for sending messages about parameter updates to the `IcedEditor`. + parameter_updates_sender: channel::Sender, + parameter_updates_receiver: Arc>, } impl Editor for IcedEditorWrapper { @@ -167,15 +185,12 @@ impl Editor for IcedEditorWrapper { parent: ParentWindowHandle, context: Arc, ) -> Box { - // FIXME: Somehow get the context/parametersetter to the GUI. Another trait that adds a - // `set_context()` would be the easiest way but perhaps not the cleanest. - let (unscaled_width, unscaled_height) = self.iced_state.size(); let scaling_factor = self.scaling_factor.load(); // TODO: iced_baseview does not have gracefuly error handling for context creation failures. // This will panic if the context could not be created. - let window = IcedWindow::>::open_parented( + let window = IcedWindow::>::open_parented( &parent, Settings { window: WindowOpenOptions { @@ -213,7 +228,11 @@ impl Editor for IcedEditorWrapper { gl_config: None, }, // We use this wrapper to be able to pass the GUI context to the editor - flags: (context, self.initialization_flags.clone()), + flags: ( + context, + self.parameter_updates_receiver.clone(), + self.initialization_flags.clone(), + ), }, ); @@ -232,6 +251,14 @@ impl Editor for IcedEditorWrapper { self.scaling_factor.store(Some(factor)); true } + + fn param_values_changed(&self) { + if self.iced_state.is_open() { + // If there's already a paramter change notification in the channel then we don't need + // to do anything else. This avoids queueing up redundant GUI redraws. + let _ = self.parameter_updates_sender.try_send(ParameterUpdate); + } + } } /// The window handle used for [`IcedEditorWrapper`]. @@ -251,57 +278,3 @@ impl Drop for IcedEditorHandle { self.window.close_window(); } } - -/// Wraps an `iced_baseview` [`Application`] around [`IcedEditor`]. Needed to allow editors to -/// always receive a copy of the GUI context. -struct IcedEditorWrapperApplication { - editor: E, -} - -impl Application for IcedEditorWrapperApplication { - type Executor = E::Executor; - type Message = E::Message; - type Flags = (Arc, E::InitializationFlags); - - fn new((context, flags): Self::Flags) -> (Self, Command) { - let (editor, command) = E::new(flags, context); - (Self { editor }, command) - } - - #[inline] - fn update( - &mut self, - window: &mut WindowQueue, - message: Self::Message, - ) -> Command { - self.editor.update(window, message) - } - - #[inline] - fn subscription( - &self, - window_subs: &mut WindowSubs, - ) -> Subscription { - self.editor.subscription(window_subs) - } - - #[inline] - fn view(&mut self) -> Element<'_, Self::Message> { - self.editor.view() - } - - #[inline] - fn background_color(&self) -> Color { - self.editor.background_color() - } - - #[inline] - fn scale_policy(&self) -> WindowScalePolicy { - self.editor.scale_policy() - } - - #[inline] - fn renderer_settings() -> iced_baseview::renderer::settings::Settings { - E::renderer_settings() - } -} diff --git a/nih_plug_iced/src/wrapper.rs b/nih_plug_iced/src/wrapper.rs new file mode 100644 index 00000000..f6ac4e0c --- /dev/null +++ b/nih_plug_iced/src/wrapper.rs @@ -0,0 +1,158 @@ +//! An [`Application`] wrapper around an [`IcedEditor`] to bridge between `iced_baseview` and +//! `nih_plug_iced`. + +use crossbeam::channel; +use nih_plug::prelude::GuiContext; +use std::sync::Arc; + +use crate::{ + futures, subscription, Application, Color, Command, Element, IcedEditor, Subscription, + WindowQueue, WindowScalePolicy, WindowSubs, +}; + +/// Wraps an `iced_baseview` [`Application`] around [`IcedEditor`]. Needed to allow editors to +/// always receive a copy of the GUI context. +pub(crate) struct IcedEditorWrapperApplication { + editor: E, + + /// We will receive notifications about parameters being changed on here. Whenever a parameter + /// update gets sent, we will trigger a [`Message::parameterUpdate`] which causes the UI to be + /// redrawn. + parameter_updates_receiver: Arc>, +} + +/// This wraps around `E::Message` to add a parmaeter update message which can be handled directly +/// by this wrapper. That parameter update message simply forces a redraw of the GUI whenever there +/// is a parameter update. +pub enum Message { + EditorMessage(E::Message), + ParameterUpdate, +} + +impl std::fmt::Debug for Message { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::EditorMessage(arg0) => f.debug_tuple("EditorMessage").field(arg0).finish(), + Self::ParameterUpdate => write!(f, "ParameterUpdate"), + } + } +} + +impl Clone for Message { + fn clone(&self) -> Self { + match self { + Self::EditorMessage(arg0) => Self::EditorMessage(arg0.clone()), + Self::ParameterUpdate => Self::ParameterUpdate, + } + } +} + +impl Application for IcedEditorWrapperApplication { + type Executor = E::Executor; + type Message = Message; + type Flags = ( + Arc, + Arc>, + E::InitializationFlags, + ); + + fn new( + (context, parameter_updates_receiver, flags): Self::Flags, + ) -> (Self, Command) { + let (editor, command) = E::new(flags, context); + + ( + Self { + editor, + parameter_updates_receiver, + }, + command.map(Message::EditorMessage), + ) + } + + #[inline] + fn update( + &mut self, + window: &mut WindowQueue, + message: Self::Message, + ) -> Command { + match message { + Message::EditorMessage(message) => self + .editor + .update(window, message) + .map(Message::EditorMessage), + // This message only exists to force a redraw + Message::ParameterUpdate => Command::none(), + } + } + + #[inline] + fn subscription( + &self, + window_subs: &mut WindowSubs, + ) -> Subscription { + // Since we're wrapping around `E::Message`, we need to do this transformation ourselves + let mut editor_window_subs = WindowSubs { + on_frame: match &window_subs.on_frame { + Some(Message::EditorMessage(message)) => Some(message.clone()), + _ => None, + }, + on_window_will_close: match &window_subs.on_window_will_close { + Some(Message::EditorMessage(message)) => Some(message.clone()), + _ => None, + }, + }; + + let subscription = Subscription::batch([ + // For some reason there's no adapter to just convert `futures::channel::mpsc::Receiver` + // into a stream that doesn't require consuming that receiver (which wouldn't work in + // this case since the subscriptions function gets called repeatedly). So we'll just use + // a crossbeam queue and this unfold instead. + subscription::unfold( + "parameter updates", + self.parameter_updates_receiver.clone(), + |parameter_updates_receiver| { + futures::future::ready(( + parameter_updates_receiver + .try_recv() + .ok() + .map(|_| Message::ParameterUpdate), + parameter_updates_receiver, + )) + }, + ), + self.editor + .subscription(&mut editor_window_subs) + .map(Message::EditorMessage), + ]); + + if let Some(message) = editor_window_subs.on_frame { + window_subs.on_frame = Some(Message::EditorMessage(message)); + } + if let Some(message) = editor_window_subs.on_window_will_close { + window_subs.on_window_will_close = Some(Message::EditorMessage(message)); + } + + subscription + } + + #[inline] + fn view(&mut self) -> Element<'_, Self::Message> { + self.editor.view().map(Message::EditorMessage) + } + + #[inline] + fn background_color(&self) -> Color { + self.editor.background_color() + } + + #[inline] + fn scale_policy(&self) -> WindowScalePolicy { + self.editor.scale_policy() + } + + #[inline] + fn renderer_settings() -> iced_baseview::renderer::settings::Settings { + E::renderer_settings() + } +} diff --git a/src/plugin.rs b/src/plugin.rs index 30112efb..2a873dec 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -243,6 +243,15 @@ pub trait Editor: Send + Sync { /// there. fn set_scale_factor(&self, factor: f32) -> bool; + /// A callback that will be called wheneer the parameter values changed while the editor is + /// open. You don't need to do anything with this, but this can be used to force a redraw when + /// the host sends a new value for a parameter or when a parameter change sent to the host gets + /// processed. + /// + /// This function will be called from the **audio thread**. It must thus be lock-free and may + /// not allocate. + fn param_values_changed(&self); + // 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