use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{
parse_macro_input, Data, DeriveInput, Field, Type,
};
#[proc_macro_derive(EguiWidget, attributes(egui))]
pub fn derive_egui_widget(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = match &input.data {
Data::Struct(data_struct) => match &data_struct.fields {
syn::Fields::Named(fields_named) => &fields_named.named,
_ => panic!("Only named structs with named fields are supported"),
},
_ => panic!("Only structs are supported"),
};
let field_impls = fields.iter().map(|field| impl_field_ui(field));
let expanded = quote! {
impl #name {
pub fn ui(&mut self, ui: &mut egui::Ui) {
#(#field_impls)*
}
pub fn show(self, ui: &mut egui::Ui) -> Self {
let mut s = self;
s.ui(ui);
s
}
}
};
TokenStream::from(expanded)
}
fn impl_field_ui(field: &Field) -> proc_macro2::TokenStream {
let field_name = field.ident.as_ref().unwrap();
let field_name_str = field_name.to_string();
let mut radio = false;
let mut checks = false;
let mut choices_str: Option<String> = None;
let mut width: Option<f64> = None;
let mut min_width: Option<f64> = None;
let mut max_width: Option<f64> = None;
let mut label_text: Option<String> = None;
for attr in &field.attrs {
if !attr.path().is_ident("egui") {
continue;
}
if let Err(err) = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("radio") {
radio = true;
return Ok(());
} else if meta.path.is_ident("checks") {
checks = true;
return Ok(());
}
let value_buf = meta.value()?;
if meta.path.is_ident("choices") {
let lit = value_buf.parse::<syn::LitStr>()?;
choices_str = Some(lit.value());
} else if meta.path.is_ident("width") {
let lit = value_buf.parse::<syn::LitFloat>()?;
width = Some(lit.base10_parse().unwrap_or(100.0));
} else if meta.path.is_ident("min_width") {
let lit = value_buf.parse::<syn::LitFloat>()?;
min_width = Some(lit.base10_parse().unwrap_or(0.0));
} else if meta.path.is_ident("max_width") {
let lit = value_buf.parse::<syn::LitFloat>()?;
max_width = Some(lit.base10_parse().unwrap_or(1000.0));
}else if meta.path.is_ident("text"){
let lit = value_buf.parse::<syn::LitStr>()?;
label_text = Some(lit.value());
} else {
return Err(meta.error("unsupported egui attribute"));
}
Ok(())
}) {
return err.to_compile_error();
}
}
let ty = &field.ty;
let ty_str = format_type(ty);
let display_name = if let Some(ref text) = label_text {
text.clone()
} else {
field_name_str.clone()
};
let display_name = syn::LitStr::new(&display_name, Span::call_site());
if radio && ty_str == "String" {
let choices = parse_choices(&choices_str);
let radios = choices.iter().map(|choice| {
let choice_str = choice.clone();
let choice_lit = syn::LitStr::new(&choice_str, Span::call_site());
quote! {
ui.radio_value(&mut self.#field_name, #choice_str.to_string(), #choice_lit);
}
});
return quote! {
ui.horizontal(|ui|{
ui.label(#display_name);
ui.horizontal(|ui| {
#(#radios)*
});
});
};
}
if checks && is_vec_string(&ty) {
let choices = parse_choices(&choices_str);
let checkboxes = choices.iter().map(|choice| {
let temp_ident = syn::Ident::new(
&format!("__check_{}_{}", field_name, choice),
proc_macro2::Span::call_site(),
);
let choice_str = choice.clone();
let choice_lit = syn::LitStr::new(&choice_str, proc_macro2::Span::call_site());
quote! {
let mut #temp_ident = self.#field_name.contains(&#choice_str.to_string());
ui.checkbox(&mut #temp_ident, #choice_lit);
if #temp_ident && !self.#field_name.contains(&#choice_str.to_string()) {
self.#field_name.push(#choice_str.to_string());
} else if !#temp_ident && self.#field_name.contains(&#choice_str.to_string()) {
self.#field_name.retain(|s| s != #choice_str);
}
}
});
return quote! {
ui.horizontal(|ui|{
ui.label(#display_name);
#(#checkboxes)*
});
};
}
if ty_str == "String" || ty_str == "std::string::String" {
let mut text_edit = quote! {
egui::TextEdit::singleline(&mut self.#field_name)
};
if let Some(w) = width {
text_edit = quote! { #text_edit.desired_width(#w as f32) };
}
if let Some(mw) = min_width {
text_edit = quote! { #text_edit.min_size(egui::vec2(#mw as f32, 0.0)) };
}
if let Some(mw) = max_width {
if width.is_none() {
text_edit = quote! { #text_edit.desired_width(#mw as f32) };
} else {
text_edit = quote! {
{
let w = (#width as f32).min(#mw as f32);
#text_edit.desired_width(w)
}
};
}
}
return quote! {
ui.horizontal(|ui|{
ui.label(#display_name);
ui.add(#text_edit);
});
};
}
match ty_str.as_str() {
"bool" => {
quote! {
ui.checkbox(&mut self.#field_name, #display_name);
}
},
"f32"|"i32" => {
quote! {
ui.horizontal(|ui|{
ui.label(#display_name);
ui.add(egui::DragValue::new(&mut self.#field_name));
});
}
},
_ => {
quote! {
ui.label(#field_name_str)
.on_hover_text(format!("Unsupported type: {}", #ty_str));
}
}
}
}
fn parse_choices(choices: &Option<String>) -> Vec<String> {
choices
.as_deref()
.unwrap_or("")
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect()
}
fn format_type(ty: &Type) -> String {
match ty {
Type::Path(p) => {
p.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
_ => "".to_string(),
}
}
fn is_vec_string(ty: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = ty {
if type_path.path.segments.len() == 1 {
let segment = &type_path.path.segments[0];
if segment.ident == "Vec" {
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
if args.args.len() == 1 {
if let syn::GenericArgument::Type(inner_ty) = &args.args[0] {
if let syn::Type::Path(p) = inner_ty {
let last_segment = p.path.segments.last().unwrap();
return last_segment.ident == "String";
}
}
}
}
}
}
}
false
}