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
|
options. That way you can use regular Rust pattern matching when working
|
||||||
with these values without having to do any conversions yourself.
|
with these values without having to do any conversions yourself.
|
||||||
- Store additional non-parameter state for your plugin by adding any field
|
- 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"]`.
|
`Params` object and annotating them with `#[persist = "key"]`.
|
||||||
- Group your parameters into logical groups by nesting `Params` objects using
|
- Group your parameters into logical groups by nesting `Params` objects using
|
||||||
the `#[nested = "Group Name"]`attribute.
|
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
|
// JSON. The `nested` fields should also implement the `Params` trait and their fields will be
|
||||||
// inherited and added to this field's lists.
|
// 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_groups_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();
|
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
|
// 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
|
// 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
|
// 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;
|
// And the `#[nested = "..."]` attribute contains a group name we should use
|
||||||
|
let mut nested_attr: Option<String> = None;
|
||||||
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() {
|
||||||
|
@ -98,22 +101,34 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||||
};
|
};
|
||||||
} else if attr.path.is_ident("nested") {
|
} else if attr.path.is_ident("nested") {
|
||||||
match attr.parse_meta() {
|
match attr.parse_meta() {
|
||||||
Ok(syn::Meta::Path(_)) => {
|
Ok(syn::Meta::NameValue(syn::MetaNameValue {
|
||||||
if !nested {
|
lit: syn::Lit::Str(s),
|
||||||
nested = true;
|
..
|
||||||
} else {
|
})) => {
|
||||||
|
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")
|
return syn::Error::new(attr.span(), "Duplicate nested attribute")
|
||||||
.to_compile_error()
|
.to_compile_error()
|
||||||
.into();
|
.into();
|
||||||
|
} else {
|
||||||
|
nested_attr = Some(s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return syn::Error::new(
|
return syn::Error::new(
|
||||||
attr.span(),
|
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()
|
.to_compile_error()
|
||||||
.into();
|
.into()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -134,6 +149,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||||
// variant
|
// variant
|
||||||
param_mapping_insert_tokens
|
param_mapping_insert_tokens
|
||||||
.push(quote! { param_map.insert(#param_id, self.#field_name.as_ptr()); });
|
.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, });
|
param_id_string_tokens.push(quote! { #param_id, });
|
||||||
}
|
}
|
||||||
(None, Some(stable_name)) => {
|
(None, Some(stable_name)) => {
|
||||||
|
@ -193,8 +212,10 @@ pub fn derive_params(input: TokenStream) -> TokenStream {
|
||||||
(None, None) => (),
|
(None, None) => (),
|
||||||
}
|
}
|
||||||
|
|
||||||
if nested {
|
if let Some(nested_group_name) = nested_attr {
|
||||||
nested_fields_idents.push(field_name.clone());
|
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(
|
fn param_map(
|
||||||
self: std::pin::Pin<&Self>,
|
self: std::pin::Pin<&Self>,
|
||||||
) -> std::collections::HashMap<&'static str, nih_plug::param::internals::ParamPtr> {
|
) -> 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;
|
use ::nih_plug::param::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),*];
|
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||||
for nested_params in nested_fields {
|
for nested_params in nested_params_fields {
|
||||||
unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) };
|
unsafe { param_map.extend(Pin::new_unchecked(*nested_params).param_map()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
fn param_ids(self: std::pin::Pin<&Self>) -> Vec<&'static str> {
|
||||||
let mut ids = vec![#(#param_id_string_tokens)*];
|
let mut ids = vec![#(#param_id_string_tokens)*];
|
||||||
|
|
||||||
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
|
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||||
for nested_params in nested_fields {
|
for nested_params in nested_params_fields {
|
||||||
unsafe { ids.append(&mut Pin::new_unchecked(*nested_params).param_ids()) };
|
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();
|
let mut serialized = ::std::collections::HashMap::new();
|
||||||
#(#field_serialize_tokens)*
|
#(#field_serialize_tokens)*
|
||||||
|
|
||||||
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
|
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||||
for nested_params in nested_fields {
|
for nested_params in nested_params_fields {
|
||||||
unsafe { serialized.extend(Pin::new_unchecked(*nested_params).serialize_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
|
// parameter structs. An easy fix would be to use
|
||||||
// https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.drain_filter
|
// https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.drain_filter
|
||||||
// once that gets stabilized.
|
// once that gets stabilized.
|
||||||
let nested_fields: &[&dyn Params] = &[#(&self.#nested_fields_idents),*];
|
let nested_params_fields: &[&dyn Params] = &[#(&self.#nested_params_field_idents),*];
|
||||||
for nested_params in nested_fields {
|
for nested_params in nested_params_fields {
|
||||||
unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) };
|
unsafe { Pin::new_unchecked(*nested_params).deserialize_fields(serialized) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,9 @@ struct GainParams {
|
||||||
#[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
|
/// You can also nest parameter structs. These will appear as a separate nested group if your
|
||||||
/// appear as a flat list to the host.
|
/// DAW displays parameters in a tree structure.
|
||||||
#[nested]
|
#[nested = "Subparameters"]
|
||||||
pub sub_params: SubParams,
|
pub sub_params: SubParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,15 @@ struct GainParams {
|
||||||
struct SubParams {
|
struct SubParams {
|
||||||
#[id = "thing"]
|
#[id = "thing"]
|
||||||
pub nested_parameter: FloatParam,
|
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 {
|
impl Default for Gain {
|
||||||
|
@ -88,6 +97,9 @@ impl Default for GainParams {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.with_value_to_string(formatters::f32_rounded(2)),
|
.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.
|
/// Re-export for use in the [`Params`] proc-macro.
|
||||||
pub use serde_json::to_string as serialize_field;
|
pub use serde_json::to_string as serialize_field;
|
||||||
|
|
||||||
/// Describes a struct containing parameters and other persistent fields. The idea is that we can
|
/// Describes a struct containing parameters and other persistent fields.
|
||||||
/// 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.
|
|
||||||
///
|
///
|
||||||
/// The other persistent parameters should be [`PersistentField`]s containing types that can be
|
/// This trait can be derived on a struct containing [`FloatParam`][super::FloatParam] and other
|
||||||
/// serialized and deserialized with Serde. When deriving this trait, any of those fields should be
|
/// parameter fields. When deriving this trait, any of those parameter fields should have the `#[id
|
||||||
/// marked with `#[persist = "key"]`.
|
/// = "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
|
/// 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
|
/// persisting keys still need to be **unique** when usting nested parameter structs. This currently
|
||||||
/// has the following caveats:
|
/// has the following caveats:
|
||||||
///
|
///
|
||||||
|
@ -44,8 +45,13 @@ pub trait Params {
|
||||||
/// is only valid as long as this pinned object is valid.
|
/// is only valid as long as this pinned object is valid.
|
||||||
fn param_map(self: Pin<&Self>) -> HashMap<&'static str, ParamPtr>;
|
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
|
/// Contains group names for each parameter in [`param_map()`][Self::param_map()]. This is
|
||||||
/// the parameters.
|
/// 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
|
/// 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
|
/// 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
|
/// Basic functionality that needs to be implemented by a plugin. The wrappers will use this to
|
||||||
/// expose the plugin in a particular plugin format.
|
/// 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
|
/// 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:
|
/// implemented. Notable missing features include:
|
||||||
///
|
///
|
||||||
/// - Sidechain inputs
|
/// - Sidechain inputs
|
||||||
/// - Multiple output busses
|
/// - Multiple output busses
|
||||||
/// - Special handling for offline processing
|
/// - 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
|
/// - Bypass parameters, right now the plugin wrappers generates one for you but there's no way to
|
||||||
/// interact with it yet
|
/// interact with it yet
|
||||||
/// - Outputting parameter changes from the plugin
|
/// - Outputting parameter changes from the plugin
|
||||||
|
|
Loading…
Reference in a new issue