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:
parent
5f0c7d0ac7
commit
1fa2b5f74f
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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`].
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
158
nih_plug_iced/src/wrapper.rs
Normal file
158
nih_plug_iced/src/wrapper.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue