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
5 changed files with 207 additions and 60 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1396,7 +1396,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_baseview"
|
name = "iced_baseview"
|
||||||
version = "0.0.3"
|
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 = [
|
dependencies = [
|
||||||
"baseview",
|
"baseview",
|
||||||
"copypasta",
|
"copypasta",
|
||||||
|
|
|
@ -82,6 +82,7 @@ struct EguiEditor<T> {
|
||||||
egui_state: Arc<EguiState>,
|
egui_state: Arc<EguiState>,
|
||||||
/// 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>>,
|
||||||
|
/// The user's update function.
|
||||||
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
|
/// 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));
|
self.scaling_factor.store(Some(factor));
|
||||||
true
|
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`].
|
/// The window handle used for [`EguiEditor`].
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
use baseview::{Size, WindowOpenOptions, WindowScalePolicy};
|
use baseview::{Size, WindowOpenOptions, WindowScalePolicy};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
|
use crossbeam::channel;
|
||||||
use nih_plug::prelude::{Editor, GuiContext, ParentWindowHandle};
|
use nih_plug::prelude::{Editor, GuiContext, ParentWindowHandle};
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
@ -15,6 +16,7 @@ use crate::widgets::ParamMessage;
|
||||||
pub use iced_baseview::*;
|
pub use iced_baseview::*;
|
||||||
|
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
mod wrapper;
|
||||||
|
|
||||||
/// Create an [`Editor`] instance using [iced](https://github.com/iced-rs/iced). The rough idea is
|
/// 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`]
|
/// 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>,
|
iced_state: Arc<IcedState>,
|
||||||
initialization_flags: E::InitializationFlags,
|
initialization_flags: E::InitializationFlags,
|
||||||
) -> Option<Box<dyn Editor>> {
|
) -> 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> {
|
Some(Box::new(IcedEditorWrapper::<E> {
|
||||||
iced_state,
|
iced_state,
|
||||||
initialization_flags,
|
initialization_flags,
|
||||||
|
|
||||||
scaling_factor: AtomicCell::new(None),
|
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`].
|
/// An [`Editor`] implementation that renders an iced [`Application`].
|
||||||
struct IcedEditorWrapper<E: IcedEditor> {
|
struct IcedEditorWrapper<E: IcedEditor> {
|
||||||
iced_state: Arc<IcedState>,
|
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
|
/// 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.
|
/// should use the system scaling factor instead.
|
||||||
scaling_factor: AtomicCell<Option<f32>>,
|
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> {
|
impl<E: IcedEditor> Editor for IcedEditorWrapper<E> {
|
||||||
|
@ -167,15 +185,12 @@ impl<E: IcedEditor> Editor for IcedEditorWrapper<E> {
|
||||||
parent: ParentWindowHandle,
|
parent: ParentWindowHandle,
|
||||||
context: Arc<dyn GuiContext>,
|
context: Arc<dyn GuiContext>,
|
||||||
) -> Box<dyn std::any::Any + Send + Sync> {
|
) -> 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 (unscaled_width, unscaled_height) = self.iced_state.size();
|
||||||
let scaling_factor = self.scaling_factor.load();
|
let scaling_factor = self.scaling_factor.load();
|
||||||
|
|
||||||
// TODO: iced_baseview does not have gracefuly error handling for context creation failures.
|
// TODO: iced_baseview does not have gracefuly error handling for context creation failures.
|
||||||
// This will panic if the context could not be created.
|
// 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,
|
&parent,
|
||||||
Settings {
|
Settings {
|
||||||
window: WindowOpenOptions {
|
window: WindowOpenOptions {
|
||||||
|
@ -213,7 +228,11 @@ impl<E: IcedEditor> Editor for IcedEditorWrapper<E> {
|
||||||
gl_config: None,
|
gl_config: None,
|
||||||
},
|
},
|
||||||
// We use this wrapper to be able to pass the GUI context to the editor
|
// 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));
|
self.scaling_factor.store(Some(factor));
|
||||||
true
|
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`].
|
/// The window handle used for [`IcedEditorWrapper`].
|
||||||
|
@ -251,57 +278,3 @@ impl<Message: Send> Drop for IcedEditorHandle<Message> {
|
||||||
self.window.close_window();
|
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.
|
/// there.
|
||||||
fn set_scale_factor(&self, factor: f32) -> bool;
|
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
|
// 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
|
||||||
|
|
Loading…
Add table
Reference in a new issue