mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00:00
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:
parent
9303953cf8
commit
9ef9b205d9
18 changed files with 713 additions and 990 deletions
202
node-graph/node-macro/src/derive_choice_type.rs
Normal file
202
node-graph/node-macro/src/derive_choice_type.rs
Normal 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)
|
||||
}
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue