Auto-generate enum type widget boilerplate for radio buttons and dropdown menus (#2589)

* First draft of factoring out the dropdown boilerplate

* Add proc macro for enum boilerplate

* Detect whether to say `crate` or the name

* Clean up the input and naming of the enum macro

* Rename a file

* Do the rename of code too

* Use the attribute-driven selection of radio vs dropdown

* Add a metadata struct and tooltips

* Move the new traits to a better place.

* Use ChoiceType, part 1

* Use ChoiceType, part 2

* Introduce a builder API for choice widgets

* Start using the new new API

* DomainWarpType should be a dropdown still

* Handle the case where a node property can never have a socket

* Rustfmt

* Code review

* Update stable node IDs in test

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
kythyria 2025-05-01 12:14:26 +01:00 committed by GitHub
parent 9303953cf8
commit 9ef9b205d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 713 additions and 990 deletions

View file

@ -0,0 +1,202 @@
use proc_macro2::{Ident, Span, TokenStream};
use quote::quote;
use syn::parse::Parse;
use syn::{Attribute, DeriveInput, Expr, LitStr, Meta};
pub fn derive_choice_type_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
match input.data {
syn::Data::Enum(data_enum) => derive_enum(&input.attrs, input.ident, data_enum),
_ => Err(syn::Error::new(input.ident.span(), "Only enums are supported at the moment")),
}
}
struct Type {
basic_item: BasicItem,
widget_hint: WidgetHint,
}
enum WidgetHint {
Radio,
Dropdown,
}
impl Parse for WidgetHint {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let tokens: Ident = input.parse()?;
if tokens == "Radio" {
Ok(Self::Radio)
} else if tokens == "Dropdown" {
Ok(Self::Dropdown)
} else {
Err(syn::Error::new_spanned(tokens, "Widget must be either Radio or Dropdown"))
}
}
}
#[derive(Default)]
struct BasicItem {
label: String,
description: Option<String>,
icon: Option<String>,
}
impl BasicItem {
fn read_attribute(&mut self, attribute: &Attribute) -> syn::Result<()> {
if attribute.path().is_ident("label") {
let token: LitStr = attribute.parse_args()?;
self.label = token.value();
}
if attribute.path().is_ident("icon") {
let token: LitStr = attribute.parse_args()?;
self.icon = Some(token.value());
}
if attribute.path().is_ident("doc") {
if let Meta::NameValue(meta_name_value) = &attribute.meta {
if let Expr::Lit(el) = &meta_name_value.value {
if let syn::Lit::Str(token) = &el.lit {
self.description = Some(token.value());
}
}
}
}
Ok(())
}
}
struct Variant {
name: Ident,
basic_item: BasicItem,
}
fn derive_enum(enum_attributes: &[Attribute], name: Ident, input: syn::DataEnum) -> syn::Result<TokenStream> {
let mut enum_info = Type {
basic_item: BasicItem::default(),
widget_hint: WidgetHint::Dropdown,
};
for attribute in enum_attributes {
enum_info.basic_item.read_attribute(attribute)?;
if attribute.path().is_ident("widget") {
enum_info.widget_hint = attribute.parse_args()?;
}
}
let mut variants = vec![Vec::new()];
for variant in &input.variants {
let mut basic_item = BasicItem::default();
for attribute in &variant.attrs {
if attribute.path().is_ident("menu_separator") {
attribute.meta.require_path_only()?;
variants.push(Vec::new());
}
basic_item.read_attribute(attribute)?;
}
if basic_item.label.is_empty() {
basic_item.label = ident_to_label(&variant.ident);
}
variants.last_mut().unwrap().push(Variant {
name: variant.ident.clone(),
basic_item,
})
}
let display_arm: Vec<_> = variants
.iter()
.flat_map(|variants| variants.iter())
.map(|variant| {
let variant_name = &variant.name;
let variant_label = &variant.basic_item.label;
quote! { #name::#variant_name => write!(f, #variant_label), }
})
.collect();
let crate_name = proc_macro_crate::crate_name("graphene-core").map_err(|e| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("Failed to find location of graphene_core. Make sure it is imported as a dependency: {}", e),
)
})?;
let crate_name = match crate_name {
proc_macro_crate::FoundCrate::Itself => quote!(crate),
proc_macro_crate::FoundCrate::Name(name) => {
let identifier = Ident::new(&name, Span::call_site());
quote! { #identifier }
}
};
let enum_description = match &enum_info.basic_item.description {
Some(s) => {
let s = s.trim();
quote! { Some(#s) }
}
None => quote! { None },
};
let group: Vec<_> = variants
.iter()
.map(|variants| {
let items = variants
.iter()
.map(|variant| {
let vname = &variant.name;
let vname_str = variant.name.to_string();
let label = &variant.basic_item.label;
let docstring = match &variant.basic_item.description {
Some(s) => {
let s = s.trim();
quote! { Some(::alloc::borrow::Cow::Borrowed(#s)) }
}
None => quote! { None },
};
let icon = match &variant.basic_item.icon {
Some(s) => quote! { Some(::alloc::borrow::Cow::Borrowed(#s)) },
None => quote! { None },
};
quote! {
(
#name::#vname, #crate_name::registry::VariantMetadata {
name: ::alloc::borrow::Cow::Borrowed(#vname_str),
label: ::alloc::borrow::Cow::Borrowed(#label),
docstring: #docstring,
icon: #icon,
}
),
}
})
.collect::<Vec<_>>();
quote! { &[ #(#items)* ], }
})
.collect();
let widget_hint = match enum_info.widget_hint {
WidgetHint::Radio => quote! { RadioButtons },
WidgetHint::Dropdown => quote! { Dropdown },
};
Ok(quote! {
impl #crate_name::vector::misc::AsU32 for #name {
fn as_u32(&self) -> u32 {
*self as u32
}
}
impl #crate_name::registry::ChoiceTypeStatic for #name {
const WIDGET_HINT: #crate_name::registry::ChoiceWidgetHint = #crate_name::registry::ChoiceWidgetHint::#widget_hint;
const DESCRIPTION: Option<&'static str> = #enum_description;
fn list() -> &'static [&'static [(Self, #crate_name::registry::VariantMetadata)]] {
&[ #(#group)* ]
}
}
impl core::fmt::Display for #name {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
#( #display_arm )*
}
}
}
})
}
fn ident_to_label(id: &Ident) -> String {
use convert_case::{Case, Casing};
id.to_string().from_case(Case::Pascal).to_case(Case::Title)
}

View file

@ -10,9 +10,24 @@ use syn::{
};
mod codegen;
mod derive_choice_type;
mod parsing;
mod validation;
/// Generate meta-information for an enum.
///
/// `#[widget(F)]` on a type indicates the type of widget to use to display/edit the type, currently `Radio` and `Dropdown` are supported.
///
/// `#[label("Foo")]` on a variant overrides the default UI label (which is otherwise the name converted to title case). All labels are collected into a [`core::fmt::Display`] impl.
///
/// `#[icon("tag"))]` sets the icon to use when a variant is shown in a menu or radio button.
///
/// Doc comments on a variant become tooltip text.
#[proc_macro_derive(ChoiceType, attributes(widget, menu_separator, label, icon))]
pub fn derive_choice_type(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_choice_type::derive_choice_type_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// A macro used to construct a proto node implementation from the given struct and the decorated function.
///
/// This works by generating two `impl` blocks for the given struct: