zng_app_proc_macros/
widget.rs

1use proc_macro2::{Span, TokenStream, TokenTree};
2use quote::{ToTokens, quote};
3use syn::{ext::IdentExt, parse::Parse, spanned::Spanned, *};
4
5use crate::{
6    util::{self, ErrorRecoverable, Errors, parse_outer_attrs},
7    widget_util::{self, WgtItem, WgtProperty, WgtWhen},
8};
9
10lazy_static! {
11    static ref DOCS_JS: String = {
12        let js = include_str!("../js/widget.js");
13        let js = minifier::js::minify(js);
14        format!("<script>{js}</script>")
15    };
16}
17
18pub fn expand(args: proc_macro::TokenStream, input: proc_macro::TokenStream, mixin: bool) -> proc_macro::TokenStream {
19    // the widget struct declaration.
20    let struct_ = parse_macro_input!(input as ItemStruct);
21    let parent;
22
23    if let Fields::Unnamed(f) = &struct_.fields {
24        if f.unnamed.len() != 1 {
25            let mut r = syn::Error::new(struct_.fields.span(), "expected `struct Name(Parent);`")
26                .to_compile_error()
27                .to_token_stream();
28            struct_.to_tokens(&mut r);
29            return r.into();
30        }
31        parent = f.unnamed[0].to_token_stream();
32    } else {
33        let mut r = syn::Error::new(struct_.fields.span(), "expected `struct Name(Parent);`")
34            .to_compile_error()
35            .to_token_stream();
36        struct_.to_tokens(&mut r);
37        return r.into();
38    }
39
40    let crate_core = util::crate_core();
41
42    let (mixin_p, mixin_p_bounded) = if mixin {
43        if struct_.generics.params.is_empty() {
44            let mut r = syn::Error::new(struct_.ident.span(), "mix-ins must have one generic `P`")
45                .to_compile_error()
46                .to_token_stream();
47            struct_.to_tokens(&mut r);
48            return r.into();
49        } else if struct_.generics.params.len() > 1 {
50            let mut r = syn::Error::new(struct_.generics.span(), "mix-ins must have one generic `P` only")
51                .to_compile_error()
52                .to_token_stream();
53            struct_.to_tokens(&mut r);
54            return r.into();
55        }
56        match struct_.generics.params.first().unwrap() {
57            GenericParam::Lifetime(l) => {
58                let mut r = syn::Error::new(l.span(), "mix-ins must have one generic `P` only")
59                    .to_compile_error()
60                    .to_token_stream();
61                struct_.to_tokens(&mut r);
62                return r.into();
63            }
64            GenericParam::Const(c) => {
65                let mut r = syn::Error::new(c.span(), "mix-ins must have one generic `P` only")
66                    .to_compile_error()
67                    .to_token_stream();
68                struct_.to_tokens(&mut r);
69                return r.into();
70            }
71
72            GenericParam::Type(t) => {
73                if !t.bounds.is_empty() || t.default.is_some() {
74                    let mut r = syn::Error::new(t.span(), "mix-ins must have one unbounded generic `P`")
75                        .to_compile_error()
76                        .to_token_stream();
77                    struct_.to_tokens(&mut r);
78                    return r.into();
79                }
80                if let Some(where_) = &struct_.generics.where_clause {
81                    let mut r = syn::Error::new(where_.span(), "mix-ins must have one unbounded generic `P`")
82                        .to_compile_error()
83                        .to_token_stream();
84                    struct_.to_tokens(&mut r);
85                    return r.into();
86                }
87
88                let id = &t.ident;
89                (quote!(<#id>), quote!(<#id : #crate_core::widget::base::WidgetImpl>))
90            }
91        }
92    } else {
93        if !struct_.generics.params.is_empty() {
94            let mut r = syn::Error::new(struct_.generics.span(), "widgets cannot be generic")
95                .to_compile_error()
96                .to_token_stream();
97            struct_.to_tokens(&mut r);
98            return r.into();
99        }
100        (quote!(), quote!())
101    };
102
103    // a `$crate` path to the widget struct.
104    let (struct_path, custom_rules) = if mixin {
105        if !args.is_empty() {
106            let span = match syn::parse::<Args>(args) {
107                Ok(a) => a.path.span(),
108                Err(e) => e.span(),
109            };
110            let mut r = syn::Error::new(span, "mix-ins do not need a `$crate` path")
111                .to_compile_error()
112                .to_token_stream();
113            struct_.to_tokens(&mut r);
114            return r.into();
115        }
116
117        (quote!(), vec![])
118    } else {
119        match syn::parse::<Args>(args) {
120            Ok(a) => (a.path.to_token_stream(), a.custom_rules),
121            Err(e) => {
122                let mut r = e.to_compile_error().to_token_stream();
123                struct_.to_tokens(&mut r);
124                return r.into();
125            }
126        }
127    };
128
129    let vis = struct_.vis;
130    let ident = struct_.ident;
131    let mut attrs = util::Attributes::new(struct_.attrs);
132    if mixin {
133        attrs.tag_doc("m", "Widget mix-in struct");
134    } else {
135        attrs.tag_doc("W", "Widget struct and macro");
136    }
137    let allow_deprecated = attrs.deprecated.as_ref().map(|_| {
138        quote! {
139            #[allow(deprecated)]
140        }
141    });
142
143    let struct_token = struct_.struct_token;
144
145    let (macro_r, start_r) = if mixin {
146        (quote!(), quote!())
147    } else {
148        let struct_path_str = struct_path.to_string().replace(' ', "");
149        let struct_path_slug = path_slug(&struct_path_str);
150
151        let val_span = util::last_span(struct_path.clone());
152        let validate_path_ident = ident_spanned!(val_span=> "zzz_widget_path_{struct_path_slug}");
153        let validate_path = quote_spanned! {val_span=>
154            #[doc(hidden)]
155            #[allow(unused)]
156            #[allow(non_snake_case)]
157            mod #validate_path_ident {
158                macro_rules! #validate_path_ident {
159                    () => {
160                        #allow_deprecated
161                        use #struct_path;
162                    }
163                }
164                #validate_path_ident!{}
165            }
166        };
167
168        let custom_rules = {
169            let mut tt = quote!();
170            for widget_util::WidgetCustomRule { rule, init } in custom_rules {
171                tt.extend(quote! {
172                    (#rule) => {
173                        #struct_path! {
174                            #init
175                        }
176                    };
177                })
178            }
179            tt
180        };
181
182        let macro_ident = ident!("{}__", struct_path_slug);
183
184        // rust-analyzer does not find the macro if we don't set the call_site here.
185        let struct_path = util::set_stream_span(struct_path, Span::call_site());
186
187        let macro_new = quote! {
188            zng::__proc_macro_util::widget::widget_new! {
189                new {
190                    let mut wgt__ = #struct_path::widget_new();
191                    let wgt__ = &mut wgt__;
192                }
193                build {
194                    let wgt__ = wgt__.widget_build();
195                    wgt__
196                }
197                set { $($tt)* }
198            }
199        };
200
201        let macro_docs = if util::is_rust_analyzer() {
202            let docs = &attrs.docs;
203            quote! {
204                #(#docs)*
205            }
206        } else {
207            quote! {
208                #[doc(hidden)]
209            }
210        };
211
212        let r = quote! {
213            #macro_docs
214            #[macro_export]
215            macro_rules! #macro_ident {
216                // actual new
217                (zng_widget: $($tt:tt)*) => {
218                    #macro_new
219                };
220
221                // enforce normal syntax, property = <expr> ..
222                ($(#[$attr:meta])* $($property_path:ident)::+ = $($rest:tt)*) => {
223                    #struct_path! {
224                        zng_widget: $(#[$attr])* $($property_path)::+ = $($rest)*
225                    }
226                };
227                // enforce normal syntax, when <expr> { .. } ..
228                ($(#[$attr:meta])* when $($rest:tt)*) => {
229                    #struct_path! {
230                        zng_widget: $(#[$attr])* when $($rest)*
231                    }
232                };
233
234                // custom rules, can be (<expr>), why we need enforce some rules
235                #custom_rules
236
237                // fallback, single property shorthand or error.
238                ($($tt:tt)*) => {
239                    #struct_path! {
240                        zng_widget: $($tt)*
241                    }
242                };
243            }
244            #[doc(hidden)]
245            #[allow(unused_imports)]
246            #vis use #macro_ident as #ident;
247            #validate_path
248        };
249
250        let source_location = widget_util::source_location(&crate_core, ident.span());
251        let start_r = quote! {
252            #allow_deprecated
253            impl #mixin_p_bounded #ident #mixin_p {
254                /// Start building a new instance.
255                pub fn widget_new() -> Self {
256                    <Self as #crate_core::widget::base::WidgetImpl>::inherit(Self::widget_type())
257                }
258
259                /// Gets the widget type info.
260                pub fn widget_type() -> #crate_core::widget::builder::WidgetType {
261                    #crate_core::widget::builder::WidgetType {
262                        type_id: std::any::TypeId::of::<Self>(),
263                        path: #struct_path_str,
264                        location: #source_location,
265                    }
266                }
267            }
268        };
269
270        (r, start_r)
271    };
272
273    let docs_js = if attrs.docs.is_empty() {
274        // cause docs missing warning
275        quote!()
276    } else {
277        let docs_js = DOCS_JS.as_str();
278        quote!(#[doc=#docs_js])
279    };
280
281    let r = quote! {
282        #attrs
283        #docs_js
284        #vis #struct_token #ident #mixin_p(#parent);
285        #allow_deprecated
286        impl #mixin_p std::ops::Deref for #ident #mixin_p {
287            type Target = #parent;
288
289            fn deref(&self) -> &Self::Target {
290                &self.0
291            }
292        }
293        #allow_deprecated
294        impl #mixin_p std::ops::DerefMut for #ident #mixin_p {
295            fn deref_mut(&mut self) -> &mut Self::Target {
296                &mut self.0
297            }
298        }
299        #start_r
300
301        #[doc(hidden)]
302        #allow_deprecated
303        impl #mixin_p_bounded #crate_core::widget::base::WidgetImpl for #ident #mixin_p {
304            fn inherit(widget: #crate_core::widget::builder::WidgetType) -> Self {
305                let mut wgt = Self(<#parent as #crate_core::widget::base::WidgetImpl>::inherit(widget));
306                *#crate_core::widget::base::WidgetImpl::base(&mut wgt).widget_importance() = #crate_core::widget::builder::Importance::WIDGET;
307                {
308                    use #crate_core::widget::base::WidgetImpl;
309                    wgt.widget_intrinsic();
310                }
311                *#crate_core::widget::base::WidgetImpl::base(&mut wgt).widget_importance() = #crate_core::widget::builder::Importance::INSTANCE;
312                wgt
313            }
314
315            fn base(&mut self) -> &mut #crate_core::widget::base::WidgetBase {
316                #crate_core::widget::base::WidgetImpl::base(&mut self.0)
317            }
318
319            fn base_ref(&self) -> &#crate_core::widget::base::WidgetBase {
320                #crate_core::widget::base::WidgetImpl::base_ref(&self.0)
321            }
322
323            fn info_instance__() -> Self {
324                Self(<#parent as #crate_core::widget::base::WidgetImpl>::info_instance__())
325            }
326        }
327
328        #macro_r
329    };
330    r.into()
331}
332
333struct Args {
334    path: TokenStream,
335    custom_rules: Vec<widget_util::WidgetCustomRule>,
336}
337impl Parse for Args {
338    fn parse(input: parse::ParseStream) -> syn::Result<Self> {
339        let fork = input.fork();
340        match (fork.parse::<Token![$]>(), fork.parse::<syn::Path>()) {
341            (Ok(_), Ok(p)) => {
342                let has_custom_rules = fork.peek(token::Brace);
343                if has_custom_rules {
344                    let _ = fork.parse::<TokenTree>();
345                }
346                if fork.is_empty() {
347                    if p.segments[0].ident == "crate" {
348                        let mut raw_parts = vec![];
349                        while !input.is_empty() {
350                            raw_parts.push(input.parse::<TokenTree>().unwrap());
351                        }
352
353                        let mut path = quote!();
354                        let mut custom_rules = vec![];
355
356                        if has_custom_rules {
357                            let rules = raw_parts.pop().unwrap().to_token_stream();
358                            custom_rules = syn::parse2::<widget_util::WidgetCustomRules>(rules)?.rules;
359                        }
360                        for part in &raw_parts {
361                            part.to_tokens(&mut path);
362                        }
363
364                        Ok(Args { path, custom_rules })
365                    } else {
366                        Err(syn::Error::new(p.segments[0].ident.span(), "expected `crate`"))
367                    }
368                } else {
369                    Err(syn::Error::new(fork.span(), "unexpected token"))
370                }
371            }
372            (Ok(_), Err(e)) => {
373                if !util::span_is_call_site(e.span()) {
374                    Err(e)
375                } else {
376                    Err(syn::Error::new(util::last_span(input.parse().unwrap()), e.to_string()))
377                }
378            }
379            _ => Err(syn::Error::new(
380                input.span(),
381                "expected a macro_rules `$crate` path to this widget mod",
382            )),
383        }
384    }
385}
386
387struct Properties {
388    errors: Errors,
389    items: Vec<WgtItem>,
390}
391impl Parse for Properties {
392    fn parse(input: parse::ParseStream) -> Result<Self> {
393        let mut errors = Errors::default();
394        let mut items = vec![];
395
396        while !input.is_empty() {
397            let attrs = parse_outer_attrs(input, &mut errors);
398
399            if input.peek(widget_util::keyword::when) {
400                if let Some(mut when) = WgtWhen::parse(input, &mut errors) {
401                    when.attrs = util::Attributes::new(attrs);
402                    items.push(WgtItem::When(when));
403                }
404            } else if input.peek(Token![pub])
405                || input.peek(Ident::peek_any)
406                || input.peek(Token![crate])
407                || input.peek(Token![super])
408                || input.peek(Token![self])
409            {
410                // peek ident or path (including keywords because of super:: and self::). {
411                match input.parse::<WgtProperty>() {
412                    Ok(mut p) => {
413                        p.attrs = util::Attributes::new(attrs);
414                        if !input.is_empty() && p.semi.is_none() {
415                            errors.push("expected `;`", input.span());
416                            while !(input.is_empty()
417                                || input.peek(Ident::peek_any)
418                                || input.peek(Token![crate])
419                                || input.peek(Token![super])
420                                || input.peek(Token![self])
421                                || input.peek(Token![#]) && input.peek(token::Bracket))
422                            {
423                                // skip to next value item.
424                                let _ = input.parse::<TokenTree>();
425                            }
426                        }
427                        items.push(WgtItem::Property(p));
428                    }
429                    Err(e) => {
430                        let (recoverable, e) = e.recoverable();
431                        if recoverable {
432                            errors.push_syn(e);
433                        } else {
434                            return Err(e);
435                        }
436                    }
437                }
438            } else {
439                errors.push("expected property or when", input.span());
440
441                // suppress the "unexpected token" error from syn parse.
442                let _ = input.parse::<TokenStream>();
443            }
444        }
445
446        Ok(Properties { errors, items })
447    }
448}
449
450fn path_slug(path: &str) -> String {
451    path.replace("crate", "")
452        .replace("::", "_")
453        .replace('$', "")
454        .trim()
455        .replace(' ', "")
456}
457
458/*
459    NEW
460*/
461
462pub fn expand_new(args: proc_macro::TokenStream) -> proc_macro::TokenStream {
463    let NewArgs {
464        start,
465        end,
466        properties: mut p,
467    } = parse_macro_input!(args as NewArgs);
468
469    let core = util::crate_core();
470
471    let mut items = quote!();
472
473    for item in &p.items {
474        match item {
475            WgtItem::Property(prop) => {
476                items.extend(prop_assign(prop, &mut p.errors, false));
477            }
478            WgtItem::When(when) => {
479                let when_expr = match syn::parse2::<widget_util::WhenExpr>(when.condition_expr.clone()) {
480                    Ok(w) => w,
481                    Err(e) => {
482                        p.errors.push_syn(e);
483                        continue;
484                    }
485                };
486
487                let mut when_expr_vars = quote!();
488                let mut inputs = quote!();
489                for ((property, member), var) in when_expr.inputs {
490                    let (property, generics) = widget_util::split_path_generics(property).unwrap();
491                    let p_ident = &property.segments.last().unwrap().ident;
492                    let p_meta = ident_spanned!(p_ident.span()=> "{p_ident}_");
493                    let var_input = ident!("{var}_in__");
494                    let member_ident = ident_spanned!(property.span()=> "__w_{member}__");
495
496                    let member = match member {
497                        widget_util::WhenInputMember::Named(ident) => {
498                            let ident_str = ident.to_string();
499                            quote! {
500                                Named(#ident_str)
501                            }
502                        }
503                        widget_util::WhenInputMember::Index(i) => quote! {
504                            Index(#i)
505                        },
506                    };
507
508                    macro_rules! quote_call {
509                        (#$mtd:ident ( $($args:tt)* )) => {
510                            if property.get_ident().is_some() {
511                                quote! {
512                                    wgt__.#$mtd #generics($($args)*);
513                                }
514                            } else {
515                                quote! {
516                                    #property::#$mtd #generics(#core::widget::base::WidgetImpl::base(&mut *wgt__), $($args)*);
517                                }
518                            }
519                        }
520                    }
521
522                    let get_meta = quote_call!(#p_meta());
523
524                    when_expr_vars.extend(quote! {
525                        let (#var_input, #var) = {
526                            let meta__ = #get_meta
527                            meta__.allowed_in_when_expr();
528                            meta__.inputs #generics().#member_ident()
529                        };
530                    });
531
532                    inputs.extend(quote! {
533                        {
534                            let meta__ = #get_meta
535                            #core::widget::builder::WhenInput {
536                                property: meta__.id(),
537                                member: #core::widget::builder::WhenInputMember::#member,
538                                var: #var_input,
539                                property_default: meta__ .default_fn #generics(),
540                            }
541                        },
542                    });
543                }
544
545                let mut assigns = quote!();
546                for prop in &when.assigns {
547                    assigns.extend(prop_assign(prop, &mut p.errors, true));
548                }
549
550                let attrs = when.attrs.cfg_and_lints();
551                let expr = when_expr.expr;
552                let expr_str = &when.condition_expr_str;
553
554                let box_expr = quote_spanned! {expr.span()=>
555                    {
556                        let expr_var = #core::var::expr_var!{#expr};
557                        #core::widget::builder::when_condition_expr_var(expr_var)
558                    }
559                };
560
561                let source_location = widget_util::source_location(&core, Span::call_site());
562                items.extend(quote! {
563                    #attrs {
564                        #when_expr_vars
565                        let inputs__ = std::boxed::Box::new([
566                            #inputs
567                        ]);
568                        #core::widget::base::WidgetImpl::base(&mut *wgt__).start_when_block(
569                            inputs__,
570                            #box_expr,
571                            #expr_str,
572                            #source_location,
573                        );
574
575                        #assigns
576
577                        #core::widget::base::WidgetImpl::base(&mut *wgt__).end_when_block();
578                    }
579                });
580            }
581        }
582    }
583
584    let errors = p.errors;
585
586    let r = quote! {
587        {
588            #errors
589
590            #start
591            #items
592            #end
593        }
594    };
595
596    r.into()
597}
598
599fn prop_assign(prop: &WgtProperty, errors: &mut Errors, is_when: bool) -> TokenStream {
600    let core = util::crate_core();
601
602    let custom_expand = if prop.has_custom_attrs() {
603        prop.custom_attrs_expand(ident!("wgt__"), is_when)
604    } else {
605        quote!()
606    };
607    let attrs = prop.attrs.cfg_and_lints();
608
609    let ident = prop.ident();
610    let path = &prop.path;
611
612    let generics = &prop.generics;
613
614    macro_rules! quote_call {
615        (#$mtd:ident ( $($args:tt)* )) => {
616            if path.get_ident().is_some() {
617                quote! {
618                    wgt__.#$mtd #generics($($args)*);
619                }
620            } else {
621                quote! {
622                    #path::#$mtd #generics(#core::widget::base::WidgetImpl::base(&mut *wgt__), $($args)*);
623                }
624            }
625        }
626    }
627
628    let ident_meta = ident_spanned!(ident.span()=> "{}_", ident);
629
630    let when_check = if is_when {
631        let meta = quote_call!(#ident_meta());
632        quote! {
633            {
634                let meta__ = #meta
635                meta__.allowed_in_when_assign();
636            }
637        }
638    } else {
639        quote!()
640    };
641
642    let prop_init;
643
644    match &prop.value {
645        Some((_, val)) => match val {
646            widget_util::PropertyValue::Special(special, _) => {
647                if prop.is_unset() {
648                    let unset_ident = ident_spanned!(ident.span()=> "unset_{}", ident);
649                    prop_init = quote_call! {
650                        #unset_ident()
651                    };
652                } else {
653                    errors.push("unknown value, expected `unset!`", special.span());
654                    return quote!();
655                }
656            }
657            widget_util::PropertyValue::Unnamed(val) => {
658                prop_init = quote_call! {
659                    #ident(#val)
660                };
661            }
662            widget_util::PropertyValue::Named(_, fields) => {
663                let mut idents_sorted: Vec<_> = fields.iter().map(|f| &f.ident).collect();
664                idents_sorted.sort();
665                let idents = fields.iter().map(|f| &f.ident);
666                let values = fields.iter().map(|f| &f.expr);
667                let ident_sorted = ident_spanned!(ident.span()=> "{}__", ident);
668
669                let call = quote_call! {
670                    #ident_sorted(#(#idents_sorted),*)
671                };
672                let meta = if path.get_ident().is_some() {
673                    quote! {
674                        wgt__.#ident_meta()
675                    }
676                } else {
677                    quote! {
678                        <#core::widget::builder::WgtInfo as #path>::#ident_meta(&#core::widget::builder::WgtInfo)
679                    }
680                };
681                prop_init = quote! {
682                    {
683                        let meta__ = #meta;
684                        #(
685                            let #idents = meta__.inputs().#idents(#values);
686                        )*
687                        #call
688                    }
689                };
690            }
691        },
692        None => {
693            let ident = &prop.path.segments.last().unwrap().ident;
694            prop_init = quote_call! {
695                #ident(#ident)
696            };
697        }
698    }
699
700    quote! {
701        #attrs {
702            #when_check
703            #custom_expand
704            #prop_init
705        }
706    }
707}
708
709struct NewArgs {
710    start: TokenStream,
711    end: TokenStream,
712    properties: Properties,
713}
714impl Parse for NewArgs {
715    fn parse(input: parse::ParseStream) -> Result<Self> {
716        Ok(Self {
717            start: non_user_braced!(input, "new").parse().unwrap(),
718            end: non_user_braced!(input, "build").parse().unwrap(),
719            properties: non_user_braced!(input, "set").parse()?,
720        })
721    }
722}