orzegui 0.1.0

A proc-macro to automatically generate egui UI from struct fields
Documentation
// egui_widget_macro/src/lib.rs
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 {
            /// 显示结构体的 UI 控件,自动绑定字段
            pub fn ui(&mut self, ui: &mut egui::Ui) {
                #(#field_impls)*
            }

            /// 在 UI 中显示并返回自身(可用于内联)
            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();

    // 解析 egui 属性
    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;
        }

        // 使用 parse_nested_meta 解析 #[egui(...)]
        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(());
            }

            // ✅ 正确:meta.value() 返回 Result<&ParseBuffer, Error>
            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());
    // 1. Radio: String 字段 + radio 属性
    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)*
                });
            });
        };
    }

    // 2. Checks: Vec<String> + checks 属性
    if checks && is_vec_string(&ty) {
        let choices = parse_choices(&choices_str);
        
        // 生成每个 checkbox 的代码
        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)*
            });
        };
    }

    // 3. String: 支持宽度控制
    if ty_str == "String" || ty_str == "std::string::String" {
        let mut text_edit = quote! {
            egui::TextEdit::singleline(&mut self.#field_name)
        };

        // 设置 desired_width (f32)
        if let Some(w) = width {
            text_edit = quote! { #text_edit.desired_width(#w as f32) };
        }

        // 设置最小尺寸(替代 min_desired_width)
        if let Some(mw) = min_width {
            text_edit = quote! { #text_edit.min_size(egui::vec2(#mw as f32, 0.0)) };
        }

        // 警告:max_width 更复杂,TextEdit 没有直接 API
        // 简单做法:限制 desired_width 不超过 max_width
        if let Some(mw) = max_width {
            if width.is_none() {
                text_edit = quote! { #text_edit.desired_width(#mw as f32) };
            } else {
                // 如果同时有 width 和 max_width,取较小值
                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);
            });
        };
    }

    // 4. 其他基础类型
    match ty_str.as_str() {
        "bool" => {
            // let display_name = if let Some(ref text) = label_text {
            //     text.clone()
            // } else {
            //     field_name_str.clone()
            // };
            quote! {
                ui.checkbox(&mut self.#field_name, #display_name);
            }
        },
        "f32"|"i32" => {
            // ✅ 使用 text 属性或字段名
            // let display_name = if let Some(ref text) = label_text {
            //     text.clone()
            // } else {
            //     field_name_str.clone()
            // };
            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));
            }
        }
    }
}

// 解析 choices = "A,B,C" → Vec<String>
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(),
    }
}

// 在 impl_field_ui 外部定义
fn is_vec_string(ty: &syn::Type) -> bool {
    if let syn::Type::Path(type_path) = ty {
        // 只处理单段路径如 `Vec<String>`,忽略 `::std::vec::Vec`
        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
}