1
0
Fork 0

Add stable IDs for enum parameters

This commit is contained in:
Robbert van der Helm 2022-06-03 22:22:36 +02:00
parent d72bd56fe7
commit 449adb8bfc
4 changed files with 136 additions and 16 deletions

View file

@ -7,10 +7,6 @@ pub fn derive_enum(input: TokenStream) -> TokenStream {
let struct_name = &ast.ident;
let variants = match ast.data {
// syn::Data::Struct(syn::DataStruct {
// fields: syn::Fields::Named(named_fields),
// ..
// }) => named_fields,
syn::Data::Enum(syn::DataEnum { variants, .. }) => variants,
_ => {
return syn::Error::new(ast.span(), "Deriving Enum is only supported on enums")
@ -23,6 +19,8 @@ pub fn derive_enum(input: TokenStream) -> TokenStream {
// order, and the names are either just the variant name or a `#[name = "..."]` attribute in
// case the name should contain a space.
let mut variant_names = Vec::new();
// IDs are optional, but they must either be set for all variants or for none of them
let mut variant_ids = Vec::new();
let mut to_index_tokens = Vec::new();
let mut from_index_tokens = Vec::new();
for (variant_idx, variant) in variants.iter().enumerate() {
@ -33,6 +31,7 @@ pub fn derive_enum(input: TokenStream) -> TokenStream {
}
let mut name_attr: Option<String> = None;
let mut id_attr: Option<String> = None;
for attr in &variant.attrs {
if attr.path.is_ident("name") {
match attr.parse_meta() {
@ -57,6 +56,45 @@ pub fn derive_enum(input: TokenStream) -> TokenStream {
.into()
}
};
} else if attr.path.is_ident("id") {
match attr.parse_meta() {
Ok(syn::Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(s),
..
})) => {
if id_attr.is_none() {
id_attr = Some(s.value());
} else {
return syn::Error::new(attr.span(), "Duplicate id attribute")
.to_compile_error()
.into();
}
}
_ => {
return syn::Error::new(
attr.span(),
"The id attribute should be a key-value pair with a string argument: #[id = \"foo-bar\"]",
)
.to_compile_error()
.into()
}
};
}
}
// IDs must either be set for all variants or for none of them
match (id_attr, variant_idx == 0, variant_ids.is_empty()) {
(Some(id), true, true) | (Some(id), false, false) => {
variant_ids.push(id);
}
(None, _, true) => (),
_ => {
return syn::Error::new(
variant.span(),
"ID attributes must either be set for all variants or for none of them",
)
.to_compile_error()
.into();
}
}
@ -70,6 +108,12 @@ pub fn derive_enum(input: TokenStream) -> TokenStream {
from_index_tokens.push(quote! { #variant_idx => #struct_name::#variant_ident, });
}
let ids_tokens = if variant_ids.is_empty() {
quote! { None }
} else {
quote! { Some(&[#(#variant_ids),*]) }
};
let from_index_default_tokens = variants.first().map(|v| {
let variant_ident = &v.ident;
quote! { _ => #struct_name::#variant_ident, }
@ -81,6 +125,10 @@ pub fn derive_enum(input: TokenStream) -> TokenStream {
&[#(#variant_names),*]
}
fn ids() -> Option<&'static [&'static str]> {
#ids_tokens
}
fn to_index(self) -> usize {
match self {
#(#to_index_tokens)*

View file

@ -3,8 +3,8 @@ use proc_macro::TokenStream;
mod enums;
mod params;
/// Derive the `Enum` trait for your simple enum parameters. See `EnumParam` for more information.
#[proc_macro_derive(Enum, attributes(name))]
/// Derive the `Enum` trait for simple enum parameters. See `EnumParam` for more information.
#[proc_macro_derive(Enum, attributes(name, id))]
pub fn derive_enum(input: TokenStream) -> TokenStream {
enums::derive_enum(input)
}

View file

@ -11,11 +11,12 @@ use super::{IntParam, Param, ParamFlags, ParamMut};
// Re-export the derive macro
pub use nih_plug_derive::Enum;
/// An enum usable with `EnumParam`. This trait can be derived. Variants are identified by their
/// **declaration order**. You can freely rename the variant names, but reordering them will break
/// compatibility with existing presets. The variatn's name is used as the display name by default.
/// If you want to override this, for instance, because it needs to contain spaces, then yo ucan use
/// the `$[name = "..."]` attribute:
/// An enum usable with `EnumParam`. This trait can be derived. Variants are identified either by a
/// stable _id_ (see below), or if those are not set then they are identifier by their **declaration
/// order**. If you don't provide IDs then you can freely rename the variant names, but reordering
/// them will break compatibility with existing presets. The variant's name is used as the display
/// name by default. If you want to override this, for instance, because it needs to contain spaces,
/// then you can use the `#[name = "..."]` attribute:
///
/// ```ignore
/// #[derive(Enum)]
@ -26,12 +27,35 @@ pub use nih_plug_derive::Enum;
/// ContainsSpaces,
/// }
/// ```
///
/// IDs can be added by adding the `#[id = "..."]` attribute to each variant:
///
/// ```ignore
/// #[derive(Enum)]
/// enum Foo {
/// #[id = "bar"],
/// Bar,
/// #[id = "baz"],
/// Baz,
/// #[id = "contains-spaces"],
/// #[name = "Contains Spaces"]
/// ContainsSpaces,
/// }
/// ```
///
/// You can safely move from not using IDs to using IDs without breaking patches, but you cannot go
/// back to not using IDs after that.
pub trait Enum {
/// The human readable names for the variants. These are displayed in the GUI or parameter list,
/// and also used for parsing text back to a parameter value. The length of this slice
/// determines how many variants there are.
fn variants() -> &'static [&'static str];
/// Optional identifiers for each variant. This makes it possible to reorder variants while
/// maintaining save compatibility (automation will still break of course). The length of this
/// slice needs to be equal to [`variants()`][Self::variants()].
fn ids() -> Option<&'static [&'static str]>;
/// Get the variant index (which may not be the same as the discriminator) corresponding to the
/// active variant. The index needs to correspond to the name in
/// [`variants()`][Self::variants()].
@ -63,6 +87,11 @@ pub struct EnumParamInner {
pub(crate) inner: IntParam,
/// The human readable variant names, obtained from [Enum::variants()].
variants: &'static [&'static str],
/// Stable identifiers for the enum variants, obtained from [Enum::ids()]. These are optional,
/// but if they are set (they're either not set for any variant, or set for all variants) then
/// these identifiers are used when saving enum parameter values to the state. Otherwise the
/// index is used.
ids: Option<&'static [&'static str]>,
}
impl<T: Enum + PartialEq> Display for EnumParam<T> {
@ -278,6 +307,7 @@ impl<T: Enum + PartialEq + 'static> EnumParam<T> {
/// parameter.
pub fn new(name: impl Into<String>, default: T) -> Self {
let variants = T::variants();
let ids = T::ids();
Self {
inner: EnumParamInner {
@ -290,6 +320,7 @@ impl<T: Enum + PartialEq + 'static> EnumParam<T> {
},
),
variants,
ids,
},
_marker: PhantomData,
}
@ -341,4 +372,29 @@ impl EnumParamInner {
pub fn len(&self) -> usize {
self.variants.len()
}
/// Get the stable ID for the parameter's current value according to
/// [`unmodulated_plain_value()`][Param::unmodulated_plain_value()]. Returns `None` if this enum
/// parameter doesn't have any stable IDs.
pub fn unmodulated_plain_id(&self) -> Option<&'static str> {
let ids = &self.ids?;
// The `Enum` trait is supposed to make sure this contains enough values
Some(ids[self.unmodulated_plain_value() as usize])
}
/// Set the parameter based on a serialized stable string identifier. Return whether the ID was
/// known and the parameter was set.
pub fn set_from_id(&mut self, id: &str) -> bool {
match self
.ids
.and_then(|ids| ids.iter().position(|candidate| *candidate == id))
{
Some(index) => {
self.set_plain_value(index as i32);
true
}
None => false,
}
}
}

View file

@ -19,6 +19,8 @@ pub enum ParamValue {
F32(f32),
I32(i32),
Bool(bool),
/// Only used for enum parameters that have the `#[id = "..."]` attribute set.
String(String),
}
/// A plugin's state so it can be restored at a later point. This object can be serialized and
@ -91,10 +93,14 @@ pub(crate) unsafe fn serialize_object<'a>(
ParamValue::Bool((*p).unmodulated_plain_value()),
),
ParamPtr::EnumParam(p) => (
// Enums are serialized based on the active variant's index (which may not be
// the same as the discriminator)
// Enums are either serialized based on the active variant's index (which may not be
// the same as the discriminator), or a custom set stable string ID. The latter
// allows the variants to be reordered.
param_id_str.clone(),
ParamValue::I32((*p).unmodulated_plain_value()),
match (*p).unmodulated_plain_id() {
Some(id) => ParamValue::String(id.to_owned()),
None => ParamValue::I32((*p).unmodulated_plain_value()),
},
),
})
.collect();
@ -145,11 +151,21 @@ pub(crate) unsafe fn deserialize_object(
(ParamPtr::FloatParam(p), ParamValue::F32(v)) => (*p).set_plain_value(*v),
(ParamPtr::IntParam(p), ParamValue::I32(v)) => (*p).set_plain_value(*v),
(ParamPtr::BoolParam(p), ParamValue::Bool(v)) => (*p).set_plain_value(*v),
// Enums are serialized based on the active variant's index (which may not be the same
// as the discriminator)
// Enums are either serialized based on the active variant's index (which may not be the
// same as the discriminator), or a custom set stable string ID. The latter allows the
// variants to be reordered.
(ParamPtr::EnumParam(p), ParamValue::I32(variant_idx)) => {
(*p).set_plain_value(*variant_idx)
}
(ParamPtr::EnumParam(p), ParamValue::String(id)) => {
let deserialized_enum = (*p).set_from_id(id);
nih_debug_assert!(
deserialized_enum,
"Unknown ID {:?} for enum parameter \"{}\"",
id,
param_id_str,
);
}
(param_ptr, param_value) => {
nih_debug_assert_failure!(
"Invalid serialized value {:?} for parameter \"{}\" ({:?})",