Allow nested parameter structs
See the Parameters docstring for the caveats.
This commit is contained in:
parent
76369ad1e1
commit
be4bbd4400
|
@ -5,7 +5,7 @@ use quote::quote;
|
|||
use std::collections::HashSet;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
#[proc_macro_derive(Params, attributes(id, persist))]
|
||||
#[proc_macro_derive(Params, attributes(id, persist, nested))]
|
||||
pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||
let ast = syn::parse_macro_input!(input as syn::DeriveInput);
|
||||
|
||||
|
@ -25,16 +25,21 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
}
|
||||
};
|
||||
|
||||
// We only care about fields with `id` and `persist` attributes. For the `id` fields we'll build
|
||||
// a mapping function that creates a hashmap containing pointers to those parmaeters. For the
|
||||
// `persist` function we'll create functions that serialize and deserialize those fields
|
||||
// individually (so they can be added and removed independently of eachother) using JSON.
|
||||
// We only care about fields with `id`, `persist`, and `nested` attributes. For the `id` fields
|
||||
// we'll build a mapping function that creates a hashmap containing pointers to those
|
||||
// parmaeters. For the `persist` function we'll create functions that serialize and deserialize
|
||||
// those fields individually (so they can be added and removed independently of eachother) using
|
||||
// JSON. The `nested` fields should also implement the `Params` trait and their fields will be
|
||||
// inherited and added to this field's lists.
|
||||
let mut param_mapping_insert_tokens = Vec::new();
|
||||
let mut param_id_string_tokens = Vec::new();
|
||||
let mut field_serialize_tokens = Vec::new();
|
||||
let mut field_deserialize_tokens = Vec::new();
|
||||
let mut nested_fields_idents = Vec::new();
|
||||
|
||||
// We'll also enforce that there are no duplicate keys at compile time
|
||||
// TODO: This doesn't work for nested fields since we don't know anything about the fields on
|
||||
// the nested structs
|
||||
let mut param_ids = HashSet::new();
|
||||
let mut persist_ids = HashSet::new();
|
||||
for field in fields.named {
|
||||
|
@ -46,6 +51,7 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
// These two attributes are mutually exclusive
|
||||
let mut id_attr: Option<String> = None;
|
||||
let mut persist_attr: Option<String> = None;
|
||||
let mut nested = false;
|
||||
for attr in &field.attrs {
|
||||
if attr.path.is_ident("id") {
|
||||
match attr.parse_meta() {
|
||||
|
@ -93,6 +99,26 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
.into()
|
||||
}
|
||||
};
|
||||
} else if attr.path.is_ident("nested") {
|
||||
match attr.parse_meta() {
|
||||
Ok(syn::Meta::Path(_)) => {
|
||||
if !nested {
|
||||
nested = true;
|
||||
} else {
|
||||
return syn::Error::new(attr.span(), "Duplicate nested attribute")
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return syn::Error::new(
|
||||
attr.span(),
|
||||
"The nested attribute should not have any arguments: #[nested]",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,6 +195,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
}
|
||||
(None, None) => (),
|
||||
}
|
||||
|
||||
if nested {
|
||||
nested_fields_idents.push(field_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
quote! {
|
||||
|
@ -180,21 +210,36 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
use ::nih_plug::Param;
|
||||
|
||||
let mut param_map = std::collections::HashMap::new();
|
||||
|
||||
#(#param_mapping_insert_tokens)*
|
||||
|
||||
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
|
||||
for nested_params in nested_fields {
|
||||
unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) };
|
||||
}
|
||||
|
||||
param_map
|
||||
}
|
||||
|
||||
fn param_ids(self: std::pin::Pin<&Self>) -> &'static [&'static str] {
|
||||
&[#(#param_id_string_tokens)*]
|
||||
fn param_ids(self: std::pin::Pin<&Self>) -> Vec<&'static str> {
|
||||
let mut ids = vec![#(#param_id_string_tokens)*];
|
||||
|
||||
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
|
||||
for nested_params in nested_fields {
|
||||
unsafe { ids.append(&mut Pin::new_unchecked(*nested_params).param_ids()) };
|
||||
}
|
||||
|
||||
ids
|
||||
}
|
||||
|
||||
fn serialize_fields(&self) -> ::std::collections::HashMap<String, String> {
|
||||
let mut serialized = ::std::collections::HashMap::new();
|
||||
|
||||
#(#field_serialize_tokens)*
|
||||
|
||||
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
|
||||
for nested_params in nested_fields {
|
||||
unsafe { serialized.extend(Pin::new_unchecked(*nested_params).serialized_fields()) };
|
||||
}
|
||||
|
||||
serialized
|
||||
}
|
||||
|
||||
|
@ -202,9 +247,18 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
for (field_name, data) in serialized {
|
||||
match field_name.as_str() {
|
||||
#(#field_deserialize_tokens)*
|
||||
_ => nih_log!("Unknown field name: {}", field_name),
|
||||
_ => nih_log!("Unknown serialized field name: {} (this may not be accurate)", field_name),
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: The above warning will course give false postiives when using nested
|
||||
// parameter structs. An easy fix would be to use
|
||||
// https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.drain_filter
|
||||
// once that gets stabilized.
|
||||
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
|
||||
for nested_params in nested_fields {
|
||||
unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,8 @@ pub mod widgets;
|
|||
/// contains the GUI's intitial size, and this is kept in sync whenever the GUI gets resized. You
|
||||
/// can also use this to know if the GUI is open, so you can avoid performing potentially expensive
|
||||
/// calculations while the GUI is not open. If you want this size to be persisted when restoring a
|
||||
/// plugin instance, then you can store it in a `#[persist]` field on your parameters struct.
|
||||
/// plugin instance, then you can store it in a `#[persist = "key"]` field on your parameters
|
||||
/// struct.
|
||||
///
|
||||
/// See [EguiState::from_size()].
|
||||
//
|
||||
|
|
|
@ -19,14 +19,25 @@ struct GainParams {
|
|||
#[id = "gain"]
|
||||
pub gain: FloatParam,
|
||||
|
||||
#[id = "as_long_as_this_name_stays_constant"]
|
||||
pub the_field_name_can_change: BoolParam,
|
||||
#[id = "stable"]
|
||||
pub but_field_names_can_change: BoolParam,
|
||||
|
||||
/// This field isn't used in this exampleq, but anything written to the vector would be restored
|
||||
/// together with a preset/state file saved for this plugin. This can be useful for storign
|
||||
/// things like sample data.
|
||||
#[persist = "industry_secrets"]
|
||||
pub random_data: RwLock<Vec<f32>>,
|
||||
|
||||
/// You can also nest parameter structs. This is only for your own organization: they will still
|
||||
/// appear as a flat list to the host.
|
||||
#[nested]
|
||||
pub sub_params: SubParams,
|
||||
}
|
||||
|
||||
#[derive(Params)]
|
||||
struct SubParams {
|
||||
#[id = "thing"]
|
||||
pub nested_parameter: FloatParam,
|
||||
}
|
||||
|
||||
impl Default for Gain {
|
||||
|
@ -63,7 +74,7 @@ impl Default for GainParams {
|
|||
// // ..Default::default(),
|
||||
},
|
||||
// ...or use the builder interface:
|
||||
the_field_name_can_change: BoolParam::new("Important value", false).with_callback(
|
||||
but_field_names_can_change: BoolParam::new("Important value", false).with_callback(
|
||||
Arc::new(|_new_value: bool| {
|
||||
// If, for instance, updating this parameter would require other parts of the
|
||||
// plugin's internal state to be updated other values to also be updated, then
|
||||
|
@ -73,6 +84,18 @@ impl Default for GainParams {
|
|||
// Persisted fields can be intialized like any other fields, and they'll keep their when
|
||||
// restoring the plugin's state.
|
||||
random_data: RwLock::new(Vec::new()),
|
||||
sub_params: SubParams {
|
||||
nested_parameter: FloatParam::new(
|
||||
"Unused Nested Parameter",
|
||||
0.5,
|
||||
FloatRange::Skewed {
|
||||
min: 2.0,
|
||||
max: 2.4,
|
||||
factor: FloatRange::skew_factor(2.0),
|
||||
},
|
||||
)
|
||||
.with_value_to_string(formatters::f32_rounded(2)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,21 @@ pub use serde_json::to_string as serialize_field;
|
|||
/// assigning a unique identifier to each parameter. We can then build a mapping from those
|
||||
/// parameter IDs to the parameters using the [Params::param_map()] function. That way we can have
|
||||
/// easy to work with JUCE-style parameter objects in the plugin without needing to manually
|
||||
/// register each parameter, like you would in JUCE.
|
||||
/// register each parameter, like you would in JUCE. When deriving this trait, any of those
|
||||
/// parameters should have the `#[id = "stable"]` attribute, where `stable` is an up to 6 character
|
||||
/// (to avoid collisions) string that will be used for the parameter's internal identifier.
|
||||
///
|
||||
/// The other persistent parameters should be [PersistentField]s containing types that can be
|
||||
/// serialized and deserialized with Serde.
|
||||
/// serialized and deserialized with Serde. When deriving this trait, any of those fields should be
|
||||
/// marked with `#[persist = "key"]`.
|
||||
///
|
||||
/// And finally when deriving this trait, it is also possible to inherit the parameters from other
|
||||
/// `Params` objects by adding the `#[nested]` attribute to those fields. Parameter IDs and
|
||||
/// persisting keys still need to be **unique** when usting nested parameter structs. This currently
|
||||
/// has the following caveats:
|
||||
///
|
||||
/// - Enforcing that parameter IDs and persist keys are unique does not work across nested structs.
|
||||
/// - Deserializing persisted fields will give false positives about fields not existing.
|
||||
///
|
||||
/// Take a look at the example gain plugin to see how this should be used.
|
||||
///
|
||||
|
@ -26,8 +37,6 @@ pub use serde_json::to_string as serialize_field;
|
|||
///
|
||||
/// This implementation is safe when using from the wrapper because the plugin object needs to be
|
||||
/// pinned, and it can never outlive the wrapper.
|
||||
//
|
||||
// TODO: Add a `#[nested]` attribute for nested params objects
|
||||
pub trait Params {
|
||||
/// Create a mapping from unique parameter IDs to parameters. This is done for every parameter
|
||||
/// field marked with `#[id = "stable_name"]`. Dereferencing the pointers stored in the values
|
||||
|
@ -36,7 +45,12 @@ pub trait Params {
|
|||
|
||||
/// All parameter IDs from `param_map`, in a stable order. This order will be used to display
|
||||
/// the parameters.
|
||||
fn param_ids(self: Pin<&Self>) -> &'static [&'static str];
|
||||
///
|
||||
/// TODO: This used to be a static slice, but now that we supported nested parameter objects
|
||||
/// that's become a bit more difficult since Rust does not have a convenient way to
|
||||
/// concatenate an arbitrary number of static slices. There's probably a better way to do
|
||||
/// this.
|
||||
fn param_ids(self: Pin<&Self>) -> Vec<&'static str>;
|
||||
|
||||
/// Serialize all fields marked with `#[persist = "stable_name"]` into a hash map containing
|
||||
/// JSON-representations of those fields so they can be written to the plugin's state and
|
||||
|
|
Loading…
Reference in a new issue