1
0
Fork 0

Add an Editor callback to notify parameter changes

This will be needed to allow iced to be reactive in our model.
This commit is contained in:
Robbert van der Helm 2022-03-13 18:03:23 +01:00
parent 5f0c7d0ac7
commit 1fa2b5f74f
5 changed files with 207 additions and 60 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -82,6 +82,7 @@ struct EguiEditor<T> {
egui_state: Arc<EguiState>,
/// The plugin's state. This is kept in between editor openenings.
user_state: Arc<RwLock<T>>,
/// The user's update function.
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
@ -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<AtomicBool>` and only force a redraw when
// that boolean is set.
}
}
/// The window handle used for [`EguiEditor`].

View file

@ -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<E: IcedEditor>(
iced_state: Arc<IcedState>,
initialization_flags: E::InitializationFlags,
) -> Option<Box<dyn Editor>> {
// 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::<E> {
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<E: IcedEditor> {
iced_state: Arc<IcedState>,
@ -159,6 +173,10 @@ struct IcedEditorWrapper<E: IcedEditor> {
/// 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>>,
/// A subscription for sending messages about parameter updates to the `IcedEditor`.
parameter_updates_sender: channel::Sender<ParameterUpdate>,
parameter_updates_receiver: Arc<channel::Receiver<ParameterUpdate>>,
}
impl<E: IcedEditor> Editor for IcedEditorWrapper<E> {
@ -167,15 +185,12 @@ impl<E: IcedEditor> Editor for IcedEditorWrapper<E> {
parent: ParentWindowHandle,
context: Arc<dyn GuiContext>,
) -> Box<dyn std::any::Any + Send + Sync> {
// 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::<IcedEditorWrapperApplication<E>>::open_parented(
let window = IcedWindow::<wrapper::IcedEditorWrapperApplication<E>>::open_parented(
&parent,
Settings {
window: WindowOpenOptions {
@ -213,7 +228,11 @@ impl<E: IcedEditor> Editor for IcedEditorWrapper<E> {
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<E: IcedEditor> Editor for IcedEditorWrapper<E> {
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<Message: Send> Drop for IcedEditorHandle<Message> {
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<E> {
editor: E,
}
impl<E: IcedEditor> Application for IcedEditorWrapperApplication<E> {
type Executor = E::Executor;
type Message = E::Message;
type Flags = (Arc<dyn GuiContext>, E::InitializationFlags);
fn new((context, flags): Self::Flags) -> (Self, Command<Self::Message>) {
let (editor, command) = E::new(flags, context);
(Self { editor }, command)
}
#[inline]
fn update(
&mut self,
window: &mut WindowQueue,
message: Self::Message,
) -> Command<Self::Message> {
self.editor.update(window, message)
}
#[inline]
fn subscription(
&self,
window_subs: &mut WindowSubs<Self::Message>,
) -> Subscription<Self::Message> {
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()
}
}

View file

@ -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<E: IcedEditor> {
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<channel::Receiver<crate::ParameterUpdate>>,
}
/// 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<E: IcedEditor> {
EditorMessage(E::Message),
ParameterUpdate,
}
impl<E: IcedEditor> std::fmt::Debug for Message<E> {
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<E: IcedEditor> Clone for Message<E> {
fn clone(&self) -> Self {
match self {
Self::EditorMessage(arg0) => Self::EditorMessage(arg0.clone()),
Self::ParameterUpdate => Self::ParameterUpdate,
}
}
}
impl<E: IcedEditor> Application for IcedEditorWrapperApplication<E> {
type Executor = E::Executor;
type Message = Message<E>;
type Flags = (
Arc<dyn GuiContext>,
Arc<channel::Receiver<crate::ParameterUpdate>>,
E::InitializationFlags,
);
fn new(
(context, parameter_updates_receiver, flags): Self::Flags,
) -> (Self, Command<Self::Message>) {
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<Self::Message> {
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<Self::Message>,
) -> Subscription<Self::Message> {
// 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()
}
}

View file

@ -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