use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Fields, Generics, Ident, Type, parse_quote};
pub fn derive_for_enum(
ident: syn::Ident,
mut generics: Generics,
attrs: Vec<syn::Attribute>,
data: syn::DataEnum,
) -> TokenStream {
if data.variants.is_empty() {
return syn::Error::new_spanned(
ident,
"ImGuiReflect cannot be derived for enums with no variants",
)
.to_compile_error()
.into();
}
let mut enum_style: Option<String> = None;
for attr in attrs.iter().filter(|a| a.path().is_ident("imgui")) {
let res = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("enum_style") {
let lit: syn::LitStr = meta.value()?.parse()?;
enum_style = Some(lit.value());
return Ok(());
}
Ok(())
});
if let Err(err) = res {
return err.to_compile_error().into();
}
}
if let Some(ref style) = enum_style
&& style != "dropdown"
&& style != "radio"
{
return syn::Error::new(
ident.span(),
"imgui(enum_style = ...) must be \"dropdown\" or \"radio\"",
)
.to_compile_error()
.into();
}
let use_radio = matches!(enum_style.as_deref(), Some("radio"));
let reflect_settings_ident =
syn::Ident::new("__imgui_reflect_settings", proc_macro2::Span::call_site());
let mut labels: Vec<syn::LitStr> = Vec::new();
let mut current_index_arms = Vec::new();
let mut from_index_arms = Vec::new();
let mut radio_arms = Vec::new();
let mut payload_match_arms = Vec::new();
let mut bound_types: Vec<Type> = Vec::new();
let mut default_types: Vec<Type> = Vec::new();
for (idx, var) in data.variants.iter().enumerate() {
let v_ident = &var.ident;
let variant_segment_lit = syn::LitStr::new(&v_ident.to_string(), v_ident.span());
let mut label_override: Option<syn::LitStr> = None;
for attr in var.attrs.iter().filter(|a| a.path().is_ident("imgui")) {
let res = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let lit: syn::LitStr = meta.value()?.parse()?;
label_override = Some(lit);
return Ok(());
}
Ok(())
});
if let Err(err) = res {
return err.to_compile_error().into();
}
}
let label_lit = if let Some(l) = label_override {
l
} else {
syn::LitStr::new(&v_ident.to_string(), v_ident.span())
};
labels.push(label_lit.clone());
let idx_usize = idx;
current_index_arms.push(match &var.fields {
Fields::Unit => quote! { Self::#v_ident => #idx_usize, },
Fields::Unnamed(_) => quote! { Self::#v_ident ( .. ) => #idx_usize, },
Fields::Named(_) => quote! { Self::#v_ident { .. } => #idx_usize, },
});
radio_arms.push(quote! {
{
let active = index == #idx_usize;
if ui.radio_button_bool(#label_lit, active) {
index = #idx_usize;
changed_select = true;
}
}
});
let from_arm = match &var.fields {
Fields::Unit => quote! { #idx_usize => Self::#v_ident, },
Fields::Unnamed(fields) => {
let defaults: Vec<TokenStream2> = fields
.unnamed
.iter()
.map(|f| {
bound_types.push(f.ty.clone());
default_types.push(f.ty.clone());
quote! { ::core::default::Default::default() }
})
.collect();
quote! { #idx_usize => Self::#v_ident( #(#defaults),* ), }
}
Fields::Named(fields) => {
let defaults: Vec<TokenStream2> = fields
.named
.iter()
.filter_map(|f| {
let name = f.ident.as_ref()?;
bound_types.push(f.ty.clone());
default_types.push(f.ty.clone());
Some(quote! { #name: ::core::default::Default::default() })
})
.collect();
quote! { #idx_usize => Self::#v_ident { #(#defaults),* }, }
}
};
from_index_arms.push(from_arm);
payload_match_arms.push(match &var.fields {
Fields::Unit => quote! { Self::#v_ident => {} },
Fields::Unnamed(fields) => {
let mut patterns: Vec<TokenStream2> = Vec::new();
let mut bindings: Vec<Ident> = Vec::new();
let mut field_stmts = Vec::new();
for (field_index, field) in fields.unnamed.iter().enumerate() {
let mut skip = false;
let mut label_override: Option<syn::LitStr> = None;
let mut field_read_only = false;
for attr in field.attrs.iter().filter(|a| a.path().is_ident("imgui")) {
let res = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
skip = true;
return Ok(());
}
if meta.path.is_ident("name") {
let lit: syn::LitStr = meta.value()?.parse()?;
label_override = Some(lit);
return Ok(());
}
if meta.path.is_ident("read_only") {
field_read_only = true;
return Ok(());
}
Ok(())
});
if let Err(err) = res {
return err.to_compile_error().into();
}
}
if skip {
let unused_ident = syn::Ident::new(
&format!("_skip_{field_index}"),
proc_macro2::Span::call_site(),
);
patterns.push(quote! { #unused_ident });
continue;
}
let binding_ident = syn::Ident::new(
&format!("__field_{field_index}"),
proc_macro2::Span::call_site(),
);
patterns.push(quote! { #binding_ident });
bindings.push(binding_ident.clone());
let label_lit = label_override.unwrap_or_else(|| {
syn::LitStr::new(&field_index.to_string(), field.span())
});
let field_segment_lit = syn::LitStr::new(
&field_index.to_string(),
proc_macro2::Span::call_site(),
);
let member_key_lit = syn::LitStr::new(
&format!("{}.{}", v_ident, field_index),
v_ident.span(),
);
field_stmts.push(quote! {
let local_changed = ::dear_imgui_reflect::with_field_path_static(#field_segment_lit, || {
let __member_read_only = {
let settings = &#reflect_settings_ident;
if let Some(member) = settings.member::<Self>(#member_key_lit) {
member.read_only
} else {
false
}
};
if #field_read_only || __member_read_only {
let _disabled = ui.begin_disabled();
let changed = ::dear_imgui_reflect::ImGuiValue::imgui_value(ui, #label_lit, #binding_ident);
drop(_disabled);
changed
} else {
::dear_imgui_reflect::ImGuiValue::imgui_value(ui, #label_lit, #binding_ident)
}
});
__changed |= local_changed;
});
}
quote! {
Self::#v_ident( #(#patterns),* ) => {
::dear_imgui_reflect::with_field_path_static(#variant_segment_lit, || {
ui.indent();
#(#field_stmts)*
ui.unindent();
});
}
}
}
Fields::Named(fields) => {
let mut field_stmts = Vec::new();
let mut bindings: Vec<Ident> = Vec::new();
for field in fields.named.iter() {
let Some(name) = field.ident.as_ref() else {
continue;
};
let mut skip = false;
let mut label_override: Option<syn::LitStr> = None;
let mut field_read_only = false;
for attr in field.attrs.iter().filter(|a| a.path().is_ident("imgui")) {
let res = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
skip = true;
return Ok(());
}
if meta.path.is_ident("name") {
let lit: syn::LitStr = meta.value()?.parse()?;
label_override = Some(lit);
return Ok(());
}
if meta.path.is_ident("read_only") {
field_read_only = true;
return Ok(());
}
Ok(())
});
if let Err(err) = res {
return err.to_compile_error().into();
}
}
if skip {
continue;
}
bindings.push(name.clone());
let label_lit = label_override
.unwrap_or_else(|| syn::LitStr::new(&name.to_string(), name.span()));
let field_segment_lit =
syn::LitStr::new(&name.to_string(), proc_macro2::Span::call_site());
let member_key_lit =
syn::LitStr::new(&format!("{}.{}", v_ident, name), v_ident.span());
field_stmts.push(quote! {
let local_changed = ::dear_imgui_reflect::with_field_path_static(#field_segment_lit, || {
let __member_read_only = {
let settings = &#reflect_settings_ident;
if let Some(member) = settings.member::<Self>(#member_key_lit) {
member.read_only
} else {
false
}
};
if #field_read_only || __member_read_only {
let _disabled = ui.begin_disabled();
let changed = ::dear_imgui_reflect::ImGuiValue::imgui_value(ui, #label_lit, #name);
drop(_disabled);
changed
} else {
::dear_imgui_reflect::ImGuiValue::imgui_value(ui, #label_lit, #name)
}
});
__changed |= local_changed;
});
}
let match_pat = if bindings.is_empty() {
quote! { Self::#v_ident { .. } }
} else {
quote! { Self::#v_ident { #(#bindings),*, .. } }
};
quote! {
#match_pat => {
::dear_imgui_reflect::with_field_path_static(#variant_segment_lit, || {
ui.indent();
#(#field_stmts)*
ui.unindent();
});
}
}
}
});
}
{
let where_clause = generics.make_where_clause();
for ty in bound_types {
where_clause
.predicates
.push(parse_quote!(#ty: ::dear_imgui_reflect::ImGuiValue));
}
for ty in default_types {
where_clause
.predicates
.push(parse_quote!(#ty: ::core::default::Default));
}
}
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let labels_decl = if use_radio {
quote! {}
} else {
quote! {
let labels: &[&str] = &[#(#labels),*];
}
};
let select_widget = if use_radio {
quote! {
let mut changed_select = false;
ui.text(label);
ui.indent();
#(#radio_arms)*
ui.unindent();
}
} else {
quote! {
let mut changed_select = ui.combo_simple_string(label, &mut index, labels);
}
};
let body = quote! {
let #reflect_settings_ident = ::dear_imgui_reflect::current_settings();
#labels_decl
let mut index: usize = match self {
#(#current_index_arms)*
};
#select_widget
let mut __changed = false;
if changed_select {
let new_value = match index {
#(#from_index_arms)*
_ => return false,
};
*self = new_value;
__changed = true;
}
match self {
#(#payload_match_arms,)*
}
__changed
};
let expanded = quote! {
impl #impl_generics ::dear_imgui_reflect::ImGuiReflect for #ident #ty_generics #where_clause {
fn imgui_reflect(
&mut self,
ui: &::dear_imgui_reflect::imgui::Ui,
label: &str,
) -> bool {
#body
}
}
};
expanded.into()
}