diff --git a/nih_plug_derive/src/enums.rs b/nih_plug_derive/src/enums.rs index 6754462d..c8463dc2 100644 --- a/nih_plug_derive/src/enums.rs +++ b/nih_plug_derive/src/enums.rs @@ -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 = None; + let mut id_attr: Option = 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)* diff --git a/nih_plug_derive/src/lib.rs b/nih_plug_derive/src/lib.rs index cefe7b69..df9950d0 100644 --- a/nih_plug_derive/src/lib.rs +++ b/nih_plug_derive/src/lib.rs @@ -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) } diff --git a/src/param/enums.rs b/src/param/enums.rs index 3205835c..ec5000d4 100644 --- a/src/param/enums.rs +++ b/src/param/enums.rs @@ -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 Display for EnumParam { @@ -278,6 +307,7 @@ impl EnumParam { /// parameter. pub fn new(name: impl Into, default: T) -> Self { let variants = T::variants(); + let ids = T::ids(); Self { inner: EnumParamInner { @@ -290,6 +320,7 @@ impl EnumParam { }, ), 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, + } + } } diff --git a/src/wrapper/state.rs b/src/wrapper/state.rs index 2231d0cc..917e21e5 100644 --- a/src/wrapper/state.rs +++ b/src/wrapper/state.rs @@ -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 \"{}\" ({:?})",