use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
parse_macro_input, Data, DeriveInput, Fields, ImplItem, ImplItemFn, ItemImpl, 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()
}
enum HandlerKind {
Focus,
Blur,
Tick,
KeyChar(char),
KeyNamed(String),
}
fn resolve_key_name(name: &str) -> Option<String> {
match name {
"tab" => Some("Tab".into()),
"enter" => Some("Enter".into()),
"esc" => Some("Esc".into()),
"backspace" => Some("Backspace".into()),
"delete" => Some("Delete".into()),
"up" => Some("Up".into()),
"down" => Some("Down".into()),
"left" => Some("Left".into()),
"right" => Some("Right".into()),
"home" => Some("Home".into()),
"end" => Some("End".into()),
"pageup" => Some("PageUp".into()),
"pagedown" => Some("PageDown".into()),
"space" => Some("Char(' ')".into()),
"plus" => Some("Char('+')".into()),
"minus" => Some("Char('-')".into()),
"equals" => Some("Char('=')".into()),
"slash" => Some("Char('/')".into()),
"backslash" => Some("Char('\\\\')".into()),
"star" => Some("Char('*')".into()),
"dot" => Some("Char('.')".into()),
"comma" => Some("Char(',')".into()),
"semicolon" => Some("Char(';')".into()),
"colon" => Some("Char(':')".into()),
"bang" => Some("Char('!')".into()),
"question" => Some("Char('?')".into()),
"hash" => Some("Char('#')".into()),
"at" => Some("Char('@')".into()),
"dollar" => Some("Char('$')".into()),
"percent" => Some("Char('%')".into()),
"caret" => Some("Char('^')".into()),
"ampersand" => Some("Char('&')".into()),
"pipe" => Some("Char('|')".into()),
"tilde" => Some("Char('~')".into()),
"underscore" => Some("Char('_')".into()),
_ => None,
}
}
fn parse_handler_name(method_name: &str) -> Option<HandlerKind> {
if method_name == "on_focus" {
return Some(HandlerKind::Focus);
}
if method_name == "on_blur" {
return Some(HandlerKind::Blur);
}
if method_name == "on_tick" {
return Some(HandlerKind::Tick);
}
if let Some(rest) = method_name.strip_prefix("on_key_") {
if rest.is_empty() {
return None;
}
if rest.chars().count() == 1 {
let c = rest.chars().next().unwrap();
return Some(HandlerKind::KeyChar(c));
}
if let Some(key_name) = resolve_key_name(rest) {
return Some(HandlerKind::KeyNamed(key_name));
}
}
None
}
#[proc_macro_attribute]
pub fn event_handlers(_attr: TokenStream, item: TokenStream) -> TokenStream {
let impl_block = parse_macro_input!(item as ItemImpl);
let struct_ty = match &impl_block.self_ty.as_ref() {
syn::Type::Path(type_path) => type_path.clone(),
_ => {
return syn::Error::new_spanned(
&impl_block.self_ty,
"#[event_handlers] requires `impl TypeName`",
)
.to_compile_error()
.into();
}
};
if impl_block.trait_.is_some() {
return syn::Error::new_spanned(
&impl_block,
"#[event_handlers] must be used on an inherent impl block (`impl Foo`), \
not a trait impl (`impl Component for Foo`)",
)
.to_compile_error()
.into();
}
const TRAIT_METHODS: &[&str] = &[
"render", "event", "style", "measure", "layout", "mount", "unmount",
"update", "type_name", "id", "class", "focusable",
"for_each_child", "for_each_child_mut",
];
let mut trait_methods: Vec<&ImplItemFn> = Vec::new();
let mut handler_methods: Vec<&ImplItemFn> = Vec::new();
let mut inherent_only: Vec<&ImplItem> = Vec::new();
for item in &impl_block.items {
if let ImplItem::Fn(method) = item {
let name_str = method.sig.ident.to_string();
if name_str == "event" {
return syn::Error::new_spanned(
method,
"#[event_handlers] generates event() automatically; \
remove the manual `fn event` and use `on_*` handlers instead",
)
.to_compile_error()
.into();
}
if parse_handler_name(&name_str).is_some() {
handler_methods.push(method);
} else if TRAIT_METHODS.contains(&name_str.as_str()) {
trait_methods.push(method);
} else {
inherent_only.push(item);
}
} else {
inherent_only.push(item);
}
}
if handler_methods.is_empty() {
return syn::Error::new_spanned(
&impl_block,
"#[event_handlers] requires at least one `on_*` handler method \
(e.g. `fn on_focus(&mut self, cx: &mut EventCx)`)",
)
.to_compile_error()
.into();
}
let mut focus_calls: Vec<proc_macro2::Ident> = Vec::new();
let mut blur_calls: Vec<proc_macro2::Ident> = Vec::new();
let mut tick_calls: Vec<proc_macro2::Ident> = Vec::new();
let mut key_char_arms: Vec<(char, proc_macro2::Ident)> = Vec::new();
let mut key_named_arms: Vec<(String, proc_macro2::Ident)> = Vec::new();
for method in &handler_methods {
let name_str = method.sig.ident.to_string();
if let Some(kind) = parse_handler_name(&name_str) {
match kind {
HandlerKind::Focus => focus_calls.push(method.sig.ident.clone()),
HandlerKind::Blur => blur_calls.push(method.sig.ident.clone()),
HandlerKind::Tick => tick_calls.push(method.sig.ident.clone()),
HandlerKind::KeyChar(c) => key_char_arms.push((c, method.sig.ident.clone())),
HandlerKind::KeyNamed(name) => key_named_arms.push((name, method.sig.ident.clone())),
}
}
}
let mut arms: Vec<proc_macro2::TokenStream> = Vec::new();
if !focus_calls.is_empty() {
let calls: Vec<_> = focus_calls.iter().map(|name| quote! { self.#name(cx); }).collect();
arms.push(quote! { lv_tui::event::Event::Focus => { #(#calls)* } });
}
if !blur_calls.is_empty() {
let calls: Vec<_> = blur_calls.iter().map(|name| quote! { self.#name(cx); }).collect();
arms.push(quote! { lv_tui::event::Event::Blur => { #(#calls)* } });
}
if !tick_calls.is_empty() {
let calls: Vec<_> = tick_calls.iter().map(|name| quote! { self.#name(cx); }).collect();
arms.push(quote! { lv_tui::event::Event::Tick => { #(#calls)* } });
}
if !key_char_arms.is_empty() || !key_named_arms.is_empty() {
let mut key_arms: Vec<proc_macro2::TokenStream> = Vec::new();
for (c, method_name) in &key_char_arms {
key_arms.push(quote! {
lv_tui::event::Key::Char(#c) => { self.#method_name(cx); }
});
}
for (name, method_name) in &key_named_arms {
let key_pat: proc_macro2::TokenStream =
syn::parse_str(&format!("lv_tui::event::Key::{}", name)).unwrap();
key_arms.push(quote! {
#key_pat => { self.#method_name(cx); }
});
}
arms.push(quote! {
lv_tui::event::Event::Key(key_event) => {
match &key_event.key {
#(#key_arms)*
_ => {}
}
}
});
}
arms.push(quote! { _ => {} });
let inherent_cloned: Vec<ImplItem> = inherent_only.iter().map(|&item| item.clone()).collect();
let inherent_impl = quote! {
impl #struct_ty {
#(#inherent_cloned)*
#(#handler_methods)*
}
};
let generated_event: ImplItemFn = syn::parse_quote! {
fn event(&mut self, event: &lv_tui::event::Event, cx: &mut lv_tui::component::EventCx) {
match event {
#(#arms)*
}
}
};
let mut component_items: Vec<ImplItem> = trait_methods.iter().map(|&m| ImplItem::Fn(m.clone())).collect();
component_items.push(ImplItem::Fn(generated_event));
let component_impl = quote! {
impl lv_tui::component::Component for #struct_ty {
#(#component_items)*
}
};
let output = quote! {
#inherent_impl
#component_impl
};
output.into()
}