use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput, Fields, Meta, Type};
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();
}
};
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 });
}
}
}
}
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);
}
let output = quote! {
#(#reactive_impls)*
};
output.into()
}