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 std::collections::HashSet;
|
||||||
use syn::spanned::Spanned;
|
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 {
|
pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||||
let ast = syn::parse_macro_input!(input as syn::DeriveInput);
|
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
|
// We only care about fields with `id`, `persist`, and `nested` attributes. For the `id` fields
|
||||||
// a mapping function that creates a hashmap containing pointers to those parmaeters. For the
|
// we'll build a mapping function that creates a hashmap containing pointers to those
|
||||||
// `persist` function we'll create functions that serialize and deserialize those fields
|
// parmaeters. For the `persist` function we'll create functions that serialize and deserialize
|
||||||
// individually (so they can be added and removed independently of eachother) using JSON.
|
// 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_mapping_insert_tokens = Vec::new();
|
||||||
let mut param_id_string_tokens = Vec::new();
|
let mut param_id_string_tokens = Vec::new();
|
||||||
let mut field_serialize_tokens = Vec::new();
|
let mut field_serialize_tokens = Vec::new();
|
||||||
let mut field_deserialize_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
|
// 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 param_ids = HashSet::new();
|
||||||
let mut persist_ids = HashSet::new();
|
let mut persist_ids = HashSet::new();
|
||||||
for field in fields.named {
|
for field in fields.named {
|
||||||
|
@ -46,6 +51,7 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||||
// These two attributes are mutually exclusive
|
// These two attributes are mutually exclusive
|
||||||
let mut id_attr: Option<String> = None;
|
let mut id_attr: Option<String> = None;
|
||||||
let mut persist_attr: Option<String> = None;
|
let mut persist_attr: Option<String> = None;
|
||||||
|
let mut nested = false;
|
||||||
for attr in &field.attrs {
|
for attr in &field.attrs {
|
||||||
if attr.path.is_ident("id") {
|
if attr.path.is_ident("id") {
|
||||||
match attr.parse_meta() {
|
match attr.parse_meta() {
|
||||||
|
@ -93,6 +99,26 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||||
.into()
|
.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) => (),
|
(None, None) => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if nested {
|
||||||
|
nested_fields_idents.push(field_name.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
|
@ -180,21 +210,36 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||||
use ::nih_plug::Param;
|
use ::nih_plug::Param;
|
||||||
|
|
||||||
let mut param_map = std::collections::HashMap::new();
|
let mut param_map = std::collections::HashMap::new();
|
||||||
|
|
||||||
#(#param_mapping_insert_tokens)*
|
#(#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
|
param_map
|
||||||
}
|
}
|
||||||
|
|
||||||
fn param_ids(self: std::pin::Pin<&Self>) -> &'static [&'static str] {
|
fn param_ids(self: std::pin::Pin<&Self>) -> Vec<&'static str> {
|
||||||
&[#(#param_id_string_tokens)*]
|
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> {
|
fn serialize_fields(&self) -> ::std::collections::HashMap<String, String> {
|
||||||
let mut serialized = ::std::collections::HashMap::new();
|
let mut serialized = ::std::collections::HashMap::new();
|
||||||
|
|
||||||
#(#field_serialize_tokens)*
|
#(#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
|
serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,9 +247,18 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||||
for (field_name, data) in serialized {
|
for (field_name, data) in serialized {
|
||||||
match field_name.as_str() {
|
match field_name.as_str() {
|
||||||
#(#field_deserialize_tokens)*
|
#(#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
|
/// 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
|
/// 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
|
/// 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()].
|
/// See [EguiState::from_size()].
|
||||||
//
|
//
|
||||||
|
|
|
@ -19,14 +19,25 @@ struct GainParams {
|
||||||
#[id = "gain"]
|
#[id = "gain"]
|
||||||
pub gain: FloatParam,
|
pub gain: FloatParam,
|
||||||
|
|
||||||
#[id = "as_long_as_this_name_stays_constant"]
|
#[id = "stable"]
|
||||||
pub the_field_name_can_change: BoolParam,
|
pub but_field_names_can_change: BoolParam,
|
||||||
|
|
||||||
/// This field isn't used in this exampleq, but anything written to the vector would be restored
|
/// 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
|
/// together with a preset/state file saved for this plugin. This can be useful for storign
|
||||||
/// things like sample data.
|
/// things like sample data.
|
||||||
#[persist = "industry_secrets"]
|
#[persist = "industry_secrets"]
|
||||||
pub random_data: RwLock<Vec<f32>>,
|
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 {
|
impl Default for Gain {
|
||||||
|
@ -63,7 +74,7 @@ impl Default for GainParams {
|
||||||
// // ..Default::default(),
|
// // ..Default::default(),
|
||||||
},
|
},
|
||||||
// ...or use the builder interface:
|
// ...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| {
|
Arc::new(|_new_value: bool| {
|
||||||
// If, for instance, updating this parameter would require other parts of the
|
// 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
|
// 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
|
// Persisted fields can be intialized like any other fields, and they'll keep their when
|
||||||
// restoring the plugin's state.
|
// restoring the plugin's state.
|
||||||
random_data: RwLock::new(Vec::new()),
|
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
|
/// 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
|
/// 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
|
/// 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
|
/// 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.
|
/// 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
|
/// This implementation is safe when using from the wrapper because the plugin object needs to be
|
||||||
/// pinned, and it can never outlive the wrapper.
|
/// pinned, and it can never outlive the wrapper.
|
||||||
//
|
|
||||||
// TODO: Add a `#[nested]` attribute for nested params objects
|
|
||||||
pub trait Params {
|
pub trait Params {
|
||||||
/// Create a mapping from unique parameter IDs to parameters. This is done for every parameter
|
/// 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
|
/// 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
|
/// All parameter IDs from `param_map`, in a stable order. This order will be used to display
|
||||||
/// the parameters.
|
/// 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
|
/// 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
|
/// JSON-representations of those fields so they can be written to the plugin's state and
|
||||||
|
|
Loading…
Reference in a new issue