Add parameter groups with #[nested = "Group Name"]
This commit is contained in:
parent
c3f717480e
commit
581e5911fc
|
@ -62,7 +62,7 @@ for download links.
|
|||
options. That way you can use regular Rust pattern matching when working
|
||||
with these values without having to do any conversions yourself.
|
||||
- Store additional non-parameter state for your plugin by adding any field
|
||||
that can be serialized with [serde](https://serde.rs/) to your plugin's
|
||||
that can be serialized with [Serde](https://serde.rs/) to your plugin's
|
||||
`Params` object and annotating them with `#[persist = "key"]`.
|
||||
- Group your parameters into logical groups by nesting `Params` objects using
|
||||
the `#[nested = "Group Name"]`attribute.
|
||||
|
|
|
@ -29,10 +29,12 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
// 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_groups_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();
|
||||
let mut nested_params_field_idents: Vec<syn::Ident> = Vec::new();
|
||||
let mut nested_params_group_names: Vec<String> = 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
|
||||
|
@ -48,7 +50,8 @@ 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;
|
||||
// And the `#[nested = "..."]` attribute contains a group name we should use
|
||||
let mut nested_attr: Option<String> = None;
|
||||
for attr in &field.attrs {
|
||||
if attr.path.is_ident("id") {
|
||||
match attr.parse_meta() {
|
||||
|
@ -98,22 +101,34 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
};
|
||||
} else if attr.path.is_ident("nested") {
|
||||
match attr.parse_meta() {
|
||||
Ok(syn::Meta::Path(_)) => {
|
||||
if !nested {
|
||||
nested = true;
|
||||
} else {
|
||||
Ok(syn::Meta::NameValue(syn::MetaNameValue {
|
||||
lit: syn::Lit::Str(s),
|
||||
..
|
||||
})) => {
|
||||
let s = s.value();
|
||||
if s.is_empty() {
|
||||
return syn::Error::new(attr.span(), "Group names cannot be empty")
|
||||
.to_compile_error()
|
||||
.into();
|
||||
} else if s.contains('/') {
|
||||
return syn::Error::new(attr.span(), "Group names may not contain slashes")
|
||||
.to_compile_error()
|
||||
.into();
|
||||
} else if nested_attr.is_some() {
|
||||
return syn::Error::new(attr.span(), "Duplicate nested attribute")
|
||||
.to_compile_error()
|
||||
.into();
|
||||
} else {
|
||||
nested_attr = Some(s);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return syn::Error::new(
|
||||
attr.span(),
|
||||
"The nested attribute should not have any arguments: #[nested]",
|
||||
"The nested attribute should be a key-value pair with a string argument: #[nested = \"Group Name\"]",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
.into()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -134,6 +149,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
// variant
|
||||
param_mapping_insert_tokens
|
||||
.push(quote! { param_map.insert(#param_id, self.#field_name.as_ptr()); });
|
||||
// Top-level parameters have no group, and we'll prefix the group name specified in
|
||||
// the `#[nested = "..."]` attribute to fields coming from nested groups
|
||||
param_groups_insert_tokens
|
||||
.push(quote! { param_groups.insert(#param_id, String::new()); });
|
||||
param_id_string_tokens.push(quote! { #param_id, });
|
||||
}
|
||||
(None, Some(stable_name)) => {
|
||||
|
@ -193,8 +212,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
(None, None) => (),
|
||||
}
|
||||
|
||||
if nested {
|
||||
nested_fields_idents.push(field_name.clone());
|
||||
if let Some(nested_group_name) = nested_attr {
|
||||
nested_params_field_idents.push(field_name.clone());
|
||||
// FIXME: Generate the insertion code here
|
||||
nested_params_group_names.push(nested_group_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,25 +224,59 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
fn param_map(
|
||||
self: std::pin::Pin<&Self>,
|
||||
) -> std::collections::HashMap<&'static str, nih_plug::param::internals::ParamPtr> {
|
||||
// This may not be in scope otherwise
|
||||
// This may not be in scope otherwise, used to call .as_ptr()
|
||||
use ::nih_plug::param::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 {
|
||||
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||
for nested_params in nested_params_fields {
|
||||
unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) };
|
||||
}
|
||||
|
||||
param_map
|
||||
}
|
||||
|
||||
fn param_groups(
|
||||
self: std::pin::Pin<&Self>,
|
||||
) -> std::collections::HashMap<&'static str, String> {
|
||||
let mut param_groups = std::collections::HashMap::new();
|
||||
#(#param_groups_insert_tokens)*
|
||||
|
||||
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||
let nested_params_groups: &[&'static str] = &[#(#nested_params_group_names),*];
|
||||
for (nested_params, group_name) in
|
||||
nested_params_fields.into_iter().zip(nested_params_groups)
|
||||
{
|
||||
let nested_param_groups =
|
||||
unsafe { std::pin::Pin::new_unchecked(*nested_params).param_groups() };
|
||||
let prefixed_nested_param_groups =
|
||||
nested_param_groups
|
||||
.into_iter()
|
||||
.map(|(param_id, nested_group_name)| {
|
||||
(
|
||||
param_id,
|
||||
if nested_group_name.is_empty() {
|
||||
group_name.to_string()
|
||||
} else {
|
||||
format!("{}/{}", group_name, nested_group_name)
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
param_groups.extend(prefixed_nested_param_groups);
|
||||
}
|
||||
|
||||
param_groups
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||
for nested_params in nested_params_fields {
|
||||
unsafe { ids.append(&mut Pin::new_unchecked(*nested_params).param_ids()) };
|
||||
}
|
||||
|
||||
|
@ -232,8 +287,8 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
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 {
|
||||
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||
for nested_params in nested_params_fields {
|
||||
unsafe { serialized.extend(Pin::new_unchecked(*nested_params).serialize_fields()) };
|
||||
}
|
||||
|
||||
|
@ -252,8 +307,8 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
|||
// 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 {
|
||||
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||
for nested_params in nested_params_fields {
|
||||
unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,9 @@ struct GainParams {
|
|||
#[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]
|
||||
/// You can also nest parameter structs. These will appear as a separate nested group if your
|
||||
/// DAW displays parameters in a tree structure.
|
||||
#[nested = "Subparameters"]
|
||||
pub sub_params: SubParams,
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,15 @@ struct GainParams {
|
|||
struct SubParams {
|
||||
#[id = "thing"]
|
||||
pub nested_parameter: FloatParam,
|
||||
|
||||
#[nested = "Sub-Subparameters"]
|
||||
pub sub_sub_params: SubSubParams,
|
||||
}
|
||||
|
||||
#[derive(Params)]
|
||||
struct SubSubParams {
|
||||
#[id = "noope"]
|
||||
pub nope: FloatParam,
|
||||
}
|
||||
|
||||
impl Default for Gain {
|
||||
|
@ -88,6 +97,9 @@ impl Default for GainParams {
|
|||
},
|
||||
)
|
||||
.with_value_to_string(formatters::f32_rounded(2)),
|
||||
sub_sub_params: SubSubParams {
|
||||
nope: FloatParam::new("Nope", 0.5, FloatRange::Linear { min: 1.0, max: 2.0 }),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,21 +11,22 @@ pub use serde_json::from_str as deserialize_field;
|
|||
/// Re-export for use in the [`Params`] proc-macro.
|
||||
pub use serde_json::to_string as serialize_field;
|
||||
|
||||
/// Describes a struct containing parameters and other persistent fields. The idea is that we can
|
||||
/// have a normal struct containing [`FloatParam`][super::FloatParam] and other parameter types with
|
||||
/// attributes 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. 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.
|
||||
/// Describes a struct containing parameters and other persistent fields.
|
||||
///
|
||||
/// The other persistent parameters should be [`PersistentField`]s containing types that can be
|
||||
/// serialized and deserialized with Serde. When deriving this trait, any of those fields should be
|
||||
/// marked with `#[persist = "key"]`.
|
||||
/// This trait can be derived on a struct containing [`FloatParam`][super::FloatParam] and other
|
||||
/// parameter fields. When deriving this trait, any of those parameter fields should have the `#[id
|
||||
/// = "stable"]` attribute, where `stable` is an up to 6 character long string (to avoid collisions)
|
||||
/// that will be used to identify the parameter internall so you can safely move it around and
|
||||
/// rename the field without breaking compatibility with old presets.
|
||||
///
|
||||
/// The struct can also contain other fields that should be persisted along with the rest of the
|
||||
/// preset data. These fields should be [`PersistentField`]s annotated with the `#[persist = "key"]`
|
||||
/// attribute containing types that can be serialized and deserialized with
|
||||
/// [Serde](https://serde.rs/).
|
||||
///
|
||||
/// 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
|
||||
/// `Params` objects by adding the `#[nested = "Group Name"]` attribute to those fields. These
|
||||
/// groups will be displayed as a tree-like structure if your DAW supports it. Parameter IDs and
|
||||
/// persisting keys still need to be **unique** when usting nested parameter structs. This currently
|
||||
/// has the following caveats:
|
||||
///
|
||||
|
@ -44,8 +45,13 @@ pub trait Params {
|
|||
/// is only valid as long as this pinned object is valid.
|
||||
fn param_map(self: Pin<&Self>) -> HashMap<&'static str, ParamPtr>;
|
||||
|
||||
/// All parameter IDs from `param_map`, in a stable order. This order will be used to display
|
||||
/// the parameters.
|
||||
/// Contains group names for each parameter in [`param_map()`][Self::param_map()]. This is
|
||||
/// either an empty string for top level parameters, or a slash/delimited `"Group Name 1/Group
|
||||
/// Name 2"` string for parameters that belong to `#[nested = "Name"]` parameter objects.
|
||||
fn param_groups(self: Pin<&Self>) -> HashMap<&'static str, String>;
|
||||
|
||||
/// All parameter IDs from [`param_map()`][Self::param_map()], in a stable order. This order
|
||||
/// will be used to display the parameters.
|
||||
///
|
||||
/// 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
|
||||
|
|
|
@ -12,13 +12,15 @@ use crate::param::internals::Params;
|
|||
/// Basic functionality that needs to be implemented by a plugin. The wrappers will use this to
|
||||
/// expose the plugin in a particular plugin format.
|
||||
///
|
||||
/// The main thing you need to do is define a `[Params]` struct containing all of your parmaeters.
|
||||
/// See the trait's documentation for more information on how to do that, or check out the examples.
|
||||
///
|
||||
/// This is super basic, and lots of things I didn't need or want to use yet haven't been
|
||||
/// implemented. Notable missing features include:
|
||||
///
|
||||
/// - Sidechain inputs
|
||||
/// - Multiple output busses
|
||||
/// - Special handling for offline processing
|
||||
/// - Parameter hierarchies/groups
|
||||
/// - Bypass parameters, right now the plugin wrappers generates one for you but there's no way to
|
||||
/// interact with it yet
|
||||
/// - Outputting parameter changes from the plugin
|
||||
|
|
Loading…
Reference in a new issue