lv-tui-macros 0.3.0

Procedural macros for the lv-tui framework
Documentation
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, Meta, Type};

/// 解析 `#[reactive(paint)]` 或 `#[reactive(paint, copy)]`
fn parse_reactive_attr(meta: &Meta) -> Option<(ReactiveKind, bool)> {
    let nested = match meta {
        Meta::List(list) => &list.tokens,
        _ => return None,
    };

    let parts: Vec<String> = nested
        .to_string()
        .split(',')
        .map(|s| s.trim().to_string())
        .collect();

    let kind = match parts.first()?.as_str() {
        "paint" => ReactiveKind::Paint,
        "layout" => ReactiveKind::Layout,
        "tree" => ReactiveKind::Tree,
        _ => return None,
    };

    let copy = parts.iter().skip(1).any(|p| p == "copy");

    Some((kind, copy))
}

enum ReactiveKind {
    Paint,
    Layout,
    Tree,
}

impl ReactiveKind {
    fn invalidate_call(&self) -> proc_macro2::TokenStream {
        match self {
            ReactiveKind::Paint => quote! { cx.invalidate_paint(); },
            ReactiveKind::Layout => quote! { cx.invalidate_layout(); },
            ReactiveKind::Tree => quote! { cx.invalidate_tree(); },
        }
    }
}

struct ReactiveField {
    name: proc_macro2::Ident,
    ty: Type,
    kind: ReactiveKind,
    copy: bool,
}

#[proc_macro_derive(Component, attributes(reactive))]
pub fn derive_component(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let struct_name = &input.ident;

    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => {
                return syn::Error::new_spanned(&data.fields, "only named fields are supported")
                    .to_compile_error()
                    .into();
            }
        },
        _ => {
            return syn::Error::new_spanned(&input, "Component can only be derived for structs")
                .to_compile_error()
                .into();
        }
    };

    // 收集 #[reactive(...)] 字段
    let mut reactive_fields: Vec<ReactiveField> = Vec::new();

    for field in fields.iter() {
        for attr in &field.attrs {
            if attr.path().is_ident("reactive") {
                if let Some((kind, copy)) = parse_reactive_attr(&attr.meta) {
                    let name = field.ident.clone().unwrap();
                    let ty = field.ty.clone();
                    reactive_fields.push(ReactiveField { name, ty, kind, copy });
                }
            }
        }
    }

    // 生成 getter/setter/update 方法
    let mut reactive_impls = Vec::new();

    for rf in &reactive_fields {
        let name = &rf.name;
        let ty = &rf.ty;
        let getter = format_ident!("{}", name);
        let setter = format_ident!("set_{}", name);
        let updater = format_ident!("update_{}", name);
        let invalidate = rf.kind.invalidate_call();

        let copy_getter = if rf.copy {
            let copy_name = format_ident!("get_{}", name);
            quote! {
                pub fn #copy_name(&self) -> #ty {
                    self.#name
                }
            }
        } else {
            quote! {}
        };

        let generated = quote! {
            #[allow(dead_code)]
            impl #struct_name {
                pub fn #getter(&self) -> &#ty {
                    &self.#name
                }

                #copy_getter

                pub fn #setter(&mut self, value: #ty, cx: &mut lv_tui::component::EventCx) {
                    if self.#name != value {
                        self.#name = value;
                        #invalidate
                    }
                }

                pub fn #updater(&mut self, cx: &mut lv_tui::component::EventCx, f: impl FnOnce(&mut #ty)) {
                    let old = self.#name.clone();
                    f(&mut self.#name);
                    if self.#name != old {
                        #invalidate
                    }
                }
            }
        };

        reactive_impls.push(generated);
    }

    // Only generate reactive getters/setters in `impl StructName`.
    // The user provides the full `impl Component for StructName`.
    let output = quote! {
        #(#reactive_impls)*
    };

    output.into()
}