Skip to main content

mirui_macros/
lib.rs

1extern crate proc_macro;
2
3mod compose;
4mod compose_attr;
5mod diag;
6mod layout_reactive;
7mod vector;
8mod visit_id;
9
10use proc_macro::TokenStream;
11use quote::quote;
12use syn::parse_macro_input;
13
14mod mold;
15
16use xrune::ds_node::ds_attr::DsAttr;
17use xrune::ds_node::{DsRoot, DsTreeRef};
18use xrune::ds_rune::DsRune;
19use xrune::ds_rune::decipher::decipher;
20
21const LAYOUT_ATTRS: &[&str] = &[
22    "width",
23    "min_width",
24    "max_width",
25    "height",
26    "min_height",
27    "max_height",
28    "grow",
29    "shrink",
30    "direction",
31    "wrap",
32    "justify",
33    "align",
34    "padding",
35    "gap",
36    "row_gap",
37    "column_gap",
38    "position",
39    "left",
40    "top",
41];
42
43const STYLE_ATTRS: &[&str] = &[
44    "bg_color",
45    "text_color",
46    "border_color",
47    "border_radius",
48    "border_width",
49    "clip_children",
50    "font",
51    "font_stack",
52    "font_size",
53];
54
55const RESERVED_LAYOUT_NAMES: &[&str] = &["View", "Row", "Column", "Scroll"];
56
57#[allow(dead_code)]
58const BUILTIN_COMPONENT_NAMES: &[&str] = &[
59    "Button",
60    "Checkbox",
61    "ProgressBar",
62    "Slider",
63    "Switch",
64    "TabBar",
65    "TextInput",
66    "Image",
67    "Icon",
68    "Text",
69    "LazyList",
70    "MirrorOf",
71    "BackgroundBlur",
72    "TemporalMix",
73];
74
75fn primary_attr_for(widget_name: &str) -> Option<&'static str> {
76    match widget_name {
77        "Text" | "Button" => Some("text"),
78        "Image" => Some("src"),
79        "MirrorOf" => Some("source"),
80        _ => None,
81    }
82}
83
84#[derive(Debug, PartialEq, Eq, Clone, Copy)]
85enum WidgetKind {
86    IllegalLowercase,
87    Layout,
88    Component,
89}
90
91fn classify_widget_name(name: &syn::Ident) -> WidgetKind {
92    let s = name.to_string();
93    let first = s.chars().next().unwrap_or('_');
94    if first.is_ascii_lowercase() {
95        WidgetKind::IllegalLowercase
96    } else if RESERVED_LAYOUT_NAMES.contains(&s.as_str()) {
97        WidgetKind::Layout
98    } else {
99        WidgetKind::Component
100    }
101}
102
103fn is_known_gesture_event(name: &str) -> bool {
104    matches!(
105        name,
106        "Tap" | "LongPress" | "DragStart" | "DragMove" | "DragEnd" | "Pinch" | "Rotate"
107    )
108}
109
110struct BusinessEventEntry {
111    widget: &'static str,
112    event: &'static str,
113    handler_component: &'static str,
114    event_path: &'static str,
115    fields: &'static [&'static str],
116}
117
118const FIRST_PARTY_BUSINESS_EVENTS: &[BusinessEventEntry] = &[
119    BusinessEventEntry {
120        widget: "Slider",
121        event: "ValueChanged",
122        handler_component: "::mirui::ui::widgets::slider::SliderHandler",
123        event_path: "::mirui::ui::widgets::slider::SliderEvent::ValueChanged",
124        fields: &["new", "old"],
125    },
126    BusinessEventEntry {
127        widget: "Slider",
128        event: "DragStarted",
129        handler_component: "::mirui::ui::widgets::slider::SliderHandler",
130        event_path: "::mirui::ui::widgets::slider::SliderEvent::DragStarted",
131        fields: &[],
132    },
133    BusinessEventEntry {
134        widget: "Slider",
135        event: "DragEnded",
136        handler_component: "::mirui::ui::widgets::slider::SliderHandler",
137        event_path: "::mirui::ui::widgets::slider::SliderEvent::DragEnded",
138        fields: &[],
139    },
140    BusinessEventEntry {
141        widget: "Switch",
142        event: "Toggled",
143        handler_component: "::mirui::ui::widgets::switch::SwitchHandler",
144        event_path: "::mirui::ui::widgets::switch::SwitchEvent::Toggled",
145        fields: &["now"],
146    },
147    BusinessEventEntry {
148        widget: "Checkbox",
149        event: "Toggled",
150        handler_component: "::mirui::ui::widgets::checkbox::CheckboxHandler",
151        event_path: "::mirui::ui::widgets::checkbox::CheckboxEvent::Toggled",
152        fields: &["now"],
153    },
154    BusinessEventEntry {
155        widget: "ProgressBar",
156        event: "ValueChanged",
157        handler_component: "::mirui::ui::widgets::progress_bar::ProgressBarHandler",
158        event_path: "::mirui::ui::widgets::progress_bar::ProgressBarEvent::ValueChanged",
159        fields: &["new", "old"],
160    },
161    BusinessEventEntry {
162        widget: "TabBar",
163        event: "SelectionChanged",
164        handler_component: "::mirui::ui::widgets::tabbar::TabBarHandler",
165        event_path: "::mirui::ui::widgets::tabbar::TabBarEvent::SelectionChanged",
166        fields: &["new", "old"],
167    },
168    BusinessEventEntry {
169        widget: "TextInput",
170        event: "Changed",
171        handler_component: "::mirui::ui::widgets::text_input::TextInputHandler",
172        event_path: "::mirui::ui::widgets::text_input::TextInputEvent::Changed",
173        fields: &["len"],
174    },
175];
176
177fn lookup_business_event(widget: &str, event: &str) -> Option<&'static BusinessEventEntry> {
178    FIRST_PARTY_BUSINESS_EVENTS
179        .iter()
180        .find(|e| e.widget == widget && e.event == event)
181}
182
183fn gesture_event_fields(name: &str) -> &'static [&'static str] {
184    match name {
185        "Tap" | "LongPress" | "DragStart" => &["x", "y", "target"],
186        "DragMove" => &["x", "y", "dx", "dy", "target"],
187        "DragEnd" => &["x", "y", "vx", "vy", "target"],
188        "Pinch" => &["x", "y", "scale_delta", "target"],
189        "Rotate" => &["x", "y", "angle", "target"],
190        _ => &[],
191    }
192}
193
194fn emit_business_handler(
195    widget_var: &syn::Ident,
196    handler_component: &str,
197    group: &[(&'static BusinessEventEntry, &OnCmd)],
198    world: &proc_macro2::TokenStream,
199) -> proc_macro2::TokenStream {
200    let handler_path: syn::Path = syn::parse_str(handler_component).unwrap();
201
202    let mut arms = proc_macro2::TokenStream::new();
203    for (entry, on_cmd) in group {
204        let body = &on_cmd.body;
205        let event_path: syn::Path = syn::parse_str(entry.event_path).unwrap();
206        let field_idents: Vec<syn::Ident> = entry
207            .fields
208            .iter()
209            .map(|f| syn::Ident::new(f, proc_macro2::Span::call_site()))
210            .collect();
211        let pattern = if field_idents.is_empty() {
212            quote! { #event_path }
213        } else {
214            quote! { #event_path { #(#field_idents),* } }
215        };
216        let bindings = if field_idents.is_empty() {
217            quote! {}
218        } else {
219            quote! { let _ = ( #(#field_idents),* ); }
220        };
221        arms.extend(quote! {
222            #pattern => {
223                #bindings
224                #[allow(unused_mut, unused_variables)]
225                let mut ctx = ::mirui::input::event::HandlerCtx {
226                    world: __world,
227                    entity: __entity,
228                    event: __event,
229                };
230                let __consumed: bool =
231                    ::mirui::input::event::HandlerReturn::into_consumed({ #body });
232                __consumed
233            },
234        });
235    }
236
237    let event_ty: syn::Path = {
238        let mut ty: syn::Path = syn::parse_str(group[0].0.event_path).unwrap();
239        ty.segments.pop();
240        if let Some(pair) = ty.segments.pop() {
241            ty.segments.push_value(pair.into_value());
242        }
243        ty
244    };
245
246    quote! {
247        (#world).insert(
248            #widget_var,
249            #handler_path {
250                on_event: ::mirui::input::event::BusinessCallback::Closure(::mirui::__Rc::new(
251                    move |__world: &mut ::mirui::ecs::World,
252                          __entity: ::mirui::ecs::Entity,
253                          __event: &#event_ty|
254                          -> bool {
255                        match __event {
256                            #arms
257                            #[allow(unreachable_patterns)]
258                            _ => false,
259                        }
260                    },
261                )),
262            },
263        );
264    }
265}
266
267fn emit_event_arm(event_name: &str, group: &[&OnCmd]) -> proc_macro2::TokenStream {
268    let event_ident = syn::Ident::new(event_name, proc_macro2::Span::call_site());
269    let fields = gesture_event_fields(event_name);
270    let field_idents: Vec<syn::Ident> = fields
271        .iter()
272        .map(|f| syn::Ident::new(f, proc_macro2::Span::call_site()))
273        .collect();
274
275    if event_name == "Tap" && group.iter().any(|h| !h.args.is_empty()) {
276        return emit_tap_with_count(group, &field_idents);
277    }
278
279    let bodies = group.iter().map(|h| &h.body);
280    let used_idents: Vec<&syn::Ident> = field_idents.iter().collect();
281
282    quote! {
283        ::mirui::input::event::gesture::GestureEvent::#event_ident { #(#field_idents),* } => {
284            let _ = ( #( #used_idents ),* );
285            #(
286                {
287                    #[allow(unused_mut, unused_variables)]
288                    let mut ctx = ::mirui::input::event::HandlerCtx {
289                        world: __world,
290                        entity: __entity,
291                        event: __event,
292                    };
293                    let __consumed: bool =
294                        ::mirui::input::event::HandlerReturn::into_consumed({ #bodies });
295                    if __consumed { return true; }
296                }
297            )*
298            false
299        },
300    }
301}
302
303fn emit_tap_with_count(group: &[&OnCmd], field_idents: &[syn::Ident]) -> proc_macro2::TokenStream {
304    let mut count_arms = proc_macro2::TokenStream::new();
305    let mut default_arm: Option<proc_macro2::TokenStream> = None;
306    for h in group {
307        let body = &h.body;
308        if h.args.is_empty() {
309            default_arm = Some(quote! {
310                _ => {
311                    #[allow(unused_mut, unused_variables)]
312                    let mut ctx = ::mirui::input::event::HandlerCtx {
313                        world: __world,
314                        entity: __entity,
315                        event: __event,
316                    };
317                    let __consumed: bool =
318                        ::mirui::input::event::HandlerReturn::into_consumed({ #body });
319                    return __consumed;
320                }
321            });
322        } else if h.args.len() == 1 {
323            let arg = &h.args[0];
324            count_arms.extend(quote! {
325                __c if __c == (#arg as u8) => {
326                    #[allow(unused_mut, unused_variables)]
327                    let mut ctx = ::mirui::input::event::HandlerCtx {
328                        world: __world,
329                        entity: __entity,
330                        event: __event,
331                    };
332                    let __consumed: bool =
333                        ::mirui::input::event::HandlerReturn::into_consumed({ #body });
334                    return __consumed;
335                },
336            });
337        }
338    }
339    let default = default_arm.unwrap_or_else(|| quote! { _ => return false });
340
341    quote! {
342        ::mirui::input::event::gesture::GestureEvent::Tap { #(#field_idents),* } => {
343            let _ = ( #(#field_idents),* );
344            let __count = ::mirui::input::event::multi_tap::current_count(__world, __entity);
345            match __count {
346                #count_arms
347                #default
348            }
349        },
350    }
351}
352
353#[allow(clippy::large_enum_variant)]
354enum Cmd {
355    Widget(WidgetCmd),
356    Compose(ComposeCmd),
357    Iter(IterCmd),
358    If(IfCmd),
359    Niche(NicheCmd),
360    Match(MatchCmd),
361    CodeBlock(proc_macro2::TokenStream),
362}
363
364struct ComposeCmd {
365    function: syn::Ident,
366    var: syn::Ident,
367    args: Vec<syn::Expr>,
368    errors: Vec<proc_macro2::TokenStream>,
369    id_lookups: Vec<(syn::Ident, String)>,
370}
371
372struct OnCmd {
373    qualifier: Option<syn::Ident>,
374    name: syn::Ident,
375    args: Vec<syn::Expr>,
376    body: syn::Block,
377}
378
379impl OnCmd {
380    fn synthesised_body_from_callback(callback: &syn::Expr) -> syn::Block {
381        syn::parse_quote! {
382            {
383                (#callback)(&mut ctx)
384            }
385        }
386    }
387}
388
389struct NicheCmd {
390    name: syn::Ident,
391    is_declaration: bool,
392    mold_slot_var: Option<syn::Ident>,
393    parent_widget: Option<syn::Ident>,
394    body: Vec<Cmd>,
395}
396
397struct MatchCmd {
398    scrutinee: syn::Expr,
399    reactive: bool,
400    arms: Vec<MatchArm>,
401}
402
403struct MatchArm {
404    pat: syn::Pat,
405    body: Vec<Cmd>,
406}
407
408// A `!signal` / `!{ expr }` attribute: re-applied via a per-widget effect.
409struct ReactiveBind {
410    property: syn::Ident,
411    expr: proc_macro2::TokenStream,
412}
413
414fn reactive_read(value: &syn::Expr) -> proc_macro2::TokenStream {
415    match value {
416        syn::Expr::Path(p) => quote! { #p.get() },
417        syn::Expr::Block(b) => quote! { #b },
418        other => quote! { #other },
419    }
420}
421
422fn reactive_property(widget: &str, attr: &str) -> Option<&'static str> {
423    match attr {
424        "text" => Some("TextContent"),
425        "bg_color" => Some("BackgroundColor"),
426        "text_color" => Some("TextColor"),
427        "normal_color" if widget == "Button" => Some("ButtonNormalColor"),
428        "render_key" => Some("RenderKey"),
429        "font_size" => Some("FontSize"),
430        "direction" => Some("Direction"),
431        "width" => Some("Width"),
432        "height" => Some("Height"),
433        "paragraph" if widget == "Text" => Some("Paragraph"),
434        "path" if widget == "Text" => Some("TextPath"),
435        _ => None,
436    }
437}
438
439fn responsive_property(widget: &str, attr: &str) -> Option<&'static str> {
440    match attr {
441        "bg_color" => Some("BackgroundColor"),
442        "text_color" => Some("TextColor"),
443        "normal_color" if widget == "Button" => Some("ButtonNormalColor"),
444        "font_size" => Some("FontSize"),
445        "direction" => Some("Direction"),
446        "paragraph" if widget == "Text" => Some("Paragraph"),
447        "width" => Some("Width"),
448        "min_width" => Some("MinWidth"),
449        "max_width" => Some("MaxWidth"),
450        "height" => Some("Height"),
451        "min_height" => Some("MinHeight"),
452        "max_height" => Some("MaxHeight"),
453        "padding" => Some("Padding"),
454        "row_gap" => Some("RowGap"),
455        "column_gap" => Some("ColumnGap"),
456        "left" => Some("Left"),
457        "top" => Some("Top"),
458        _ => None,
459    }
460}
461
462struct LayoutBind {
463    property: syn::Ident,
464    value: crate::layout_reactive::LayoutValue,
465}
466
467struct WidgetCmd {
468    name: syn::Ident,
469    kind: WidgetKind,
470    var: syn::Ident,
471    attrs: Vec<proc_macro2::TokenStream>,
472    layout_fields: Vec<proc_macro2::TokenStream>,
473    errors: Vec<proc_macro2::TokenStream>,
474    enchants: Vec<proc_macro2::TokenStream>,
475    on_handlers: Vec<OnCmd>,
476    component_fields: Vec<proc_macro2::TokenStream>,
477    text_tuple_value: Option<proc_macro2::TokenStream>,
478    text_paragraph_value: Option<proc_macro2::TokenStream>,
479    id_registrations: Vec<proc_macro2::TokenStream>,
480    id_lookups: Vec<(syn::Ident, String)>,
481    reactive_binds: Vec<ReactiveBind>,
482    layout_binds: Vec<LayoutBind>,
483    children: Vec<Cmd>,
484}
485
486struct ParsedAttrs {
487    builder_calls: Vec<proc_macro2::TokenStream>,
488    layout_fields: Vec<proc_macro2::TokenStream>,
489    errors: Vec<proc_macro2::TokenStream>,
490    component_inserts: Vec<proc_macro2::TokenStream>,
491    component_fields: Vec<proc_macro2::TokenStream>,
492    text_tuple_value: Option<proc_macro2::TokenStream>,
493    text_paragraph_value: Option<proc_macro2::TokenStream>,
494    id_registrations: Vec<proc_macro2::TokenStream>,
495    id_lookups: Vec<(syn::Ident, String)>,
496    reactive_binds: Vec<ReactiveBind>,
497    layout_binds: Vec<LayoutBind>,
498    user_set_direction: bool,
499}
500
501struct IterCmd {
502    iterable: proc_macro2::TokenStream,
503    variable: syn::Ident,
504    body: Vec<Cmd>,
505    reactive: bool,
506    key: Option<proc_macro2::TokenStream>,
507}
508
509struct IfCmd {
510    branches: Vec<Branch>,
511    reactive: bool,
512}
513
514struct Branch {
515    cond: Option<syn::Expr>,
516    body: Vec<Cmd>,
517}
518
519struct MiruiRune {
520    world_expr: proc_macro2::TokenStream,
521    parent_expr: proc_macro2::TokenStream,
522    stack: Vec<Vec<Cmd>>,
523    widget_kind_stack: Vec<Option<syn::Ident>>,
524    counter: usize,
525    mold_mode: bool,
526}
527
528impl MiruiRune {
529    fn new() -> Self {
530        Self {
531            world_expr: quote! { __world },
532            parent_expr: quote! { __parent },
533            stack: vec![Vec::new()],
534            widget_kind_stack: Vec::new(),
535            counter: 0,
536            mold_mode: false,
537        }
538    }
539
540    fn new_mold(world: proc_macro2::TokenStream, entity: proc_macro2::TokenStream) -> Self {
541        Self {
542            world_expr: world,
543            parent_expr: entity,
544            stack: vec![Vec::new()],
545            widget_kind_stack: Vec::new(),
546            counter: 0,
547            mold_mode: true,
548        }
549    }
550
551    fn next_var(&mut self) -> syn::Ident {
552        let name = format!("__w{}", self.counter);
553        self.counter += 1;
554        syn::Ident::new(&name, proc_macro2::Span::call_site())
555    }
556
557    fn parse_attrs(
558        &self,
559        attrs: &[DsAttr],
560        widget_name: &str,
561        widget_kind: WidgetKind,
562    ) -> ParsedAttrs {
563        let mut builder_calls = Vec::new();
564        let mut layout_fields = Vec::new();
565        let mut errors = Vec::new();
566        let mut component_inserts = Vec::new();
567        let mut component_fields = Vec::new();
568        let mut text_tuple_value: Option<proc_macro2::TokenStream> = None;
569        let mut text_paragraph_value: Option<proc_macro2::TokenStream> = None;
570        let mut id_registrations = Vec::new();
571        let mut id_lookups: Vec<(syn::Ident, String)> = Vec::new();
572        let mut reactive_binds: Vec<ReactiveBind> = Vec::new();
573        let mut layout_binds = Vec::new();
574        let mut user_set_direction = false;
575
576        let is_text_widget = widget_name == "Text";
577        let is_button_widget = widget_name == "Button";
578        let is_text_input_widget = widget_name == "TextInput";
579        const TEXT_INPUT_FIELDS: &[&str] = &[
580            "text_color",
581            "placeholder_color",
582            "cursor_color",
583            "focus_border_color",
584        ];
585
586        let mut positional_consumed = false;
587        for attr in attrs {
588            let mut rewritten = attr.value.clone();
589            syn::visit_mut::VisitMut::visit_expr_mut(
590                &mut crate::visit_id::IdRewriter {
591                    captured: &mut id_lookups,
592                },
593                &mut rewritten,
594            );
595            let value = &rewritten;
596
597            let name = match &attr.name {
598                Some(n) => n.to_string(),
599                None => match (primary_attr_for(widget_name), positional_consumed) {
600                    (Some(primary), false) => {
601                        positional_consumed = true;
602                        primary.to_string()
603                    }
604                    (Some(_), true) => {
605                        errors.push(
606                            syn::Error::new(
607                                syn::spanned::Spanned::span(&attr.value),
608                                format!(
609                                    "{widget_name} accepts only one positional argument; pass extra fields by name",
610                                ),
611                            )
612                            .to_compile_error(),
613                        );
614                        continue;
615                    }
616                    (None, _) => {
617                        errors.push(
618                            syn::Error::new(
619                                syn::spanned::Spanned::span(&attr.value),
620                                format!(
621                                    "{widget_name} does not accept positional arguments; use `name: value` form",
622                                ),
623                            )
624                            .to_compile_error(),
625                        );
626                        continue;
627                    }
628                },
629            };
630
631            let attr_span = attr
632                .name
633                .as_ref()
634                .map(|n| n.span())
635                .unwrap_or_else(|| syn::spanned::Spanned::span(&attr.value));
636
637            if crate::layout_reactive::is_layout_placeholder(&attr.value) {
638                match responsive_property(widget_name, &name) {
639                    Some(property) => {
640                        match crate::layout_reactive::LayoutValue::parse_expr(&attr.value) {
641                            Ok(Some(value)) => layout_binds.push(LayoutBind {
642                                property: syn::Ident::new(property, attr_span),
643                                value,
644                            }),
645                            Ok(None) => unreachable!("layout placeholder already checked"),
646                            Err(error) => errors.push(error.to_compile_error()),
647                        }
648                    }
649                    None => errors.push(
650                        syn::Error::new(
651                            attr_span,
652                            format!("`{name}` does not support layout-responsive `@` binding"),
653                        )
654                        .to_compile_error(),
655                    ),
656                }
657                continue;
658            }
659
660            if name == "container" {
661                match value {
662                    syn::Expr::Lit(syn::ExprLit {
663                        lit: syn::Lit::Bool(enabled),
664                        ..
665                    }) if enabled.value => {
666                        component_inserts.push(quote! { ::mirui::ui::LayoutContainer });
667                    }
668                    _ => errors.push(
669                        syn::Error::new_spanned(value, "`container` only accepts `true`")
670                            .to_compile_error(),
671                    ),
672                }
673                continue;
674            }
675
676            // Must run before the Text / Component `text` routes below, or a
677            // reactive `text` gets frozen as static content instead of bound.
678            if attr.reactive {
679                match reactive_property(widget_name, &name) {
680                    Some(property) => {
681                        reactive_binds.push(ReactiveBind {
682                            property: syn::Ident::new(property, attr_span),
683                            expr: reactive_read(value),
684                        });
685                        continue;
686                    }
687                    None => {
688                        errors.push(
689                            syn::Error::new(
690                                attr_span,
691                                format!("`{name}` does not support reactive `$` binding"),
692                            )
693                            .to_compile_error(),
694                        );
695                        continue;
696                    }
697                }
698            }
699
700            if (is_text_widget || is_button_widget) && name == "text" {
701                text_tuple_value = Some(quote! { #value });
702                continue;
703            }
704
705            if is_text_widget && name == "paragraph" {
706                text_paragraph_value = Some(quote! { #value });
707                continue;
708            }
709
710            if is_text_widget && name == "path" {
711                builder_calls.push(quote! { .text_path(#value) });
712                continue;
713            }
714
715            if is_text_input_widget && TEXT_INPUT_FIELDS.contains(&name.as_str()) {
716                let field_ident = syn::Ident::new(&name, attr_span);
717                component_fields.push(Self::field_init_tokens(&field_ident, value));
718                continue;
719            }
720
721            if widget_kind == WidgetKind::Component && name == "text" {
722                let field_ident = syn::Ident::new(&name, attr_span);
723                component_fields.push(Self::field_init_tokens(&field_ident, value));
724                continue;
725            }
726
727            match name.as_str() {
728                "bg_color" => builder_calls.push(quote! { .bg_color(#value) }),
729                "text" => builder_calls.push(quote! { .text(#value) }),
730                "text_color" => builder_calls.push(quote! { .text_color(#value) }),
731                "border_radius" => builder_calls.push(quote! { .border_radius(#value) }),
732                "clip_children" => builder_calls.push(quote! { .clip_children(#value) }),
733                "border_color" => builder_calls.push(quote! { .border(#value, 1) }),
734                "border_width" => builder_calls.push(quote! { .border_width(#value) }),
735                "font" => builder_calls.push(quote! { .font(#value) }),
736                "font_stack" => builder_calls.push(quote! { .font_stack(#value) }),
737                "font_size" => builder_calls.push(quote! { .font_size(#value) }),
738                "width" => {
739                    layout_fields.push(quote! { width: mirui::types::Dimension::from(#value) })
740                }
741                "min_width" => {
742                    layout_fields.push(quote! { min_width: mirui::types::Dimension::from(#value) })
743                }
744                "max_width" => {
745                    layout_fields.push(quote! { max_width: mirui::types::Dimension::from(#value) })
746                }
747                "height" => {
748                    layout_fields.push(quote! { height: mirui::types::Dimension::from(#value) })
749                }
750                "min_height" => {
751                    layout_fields.push(quote! { min_height: mirui::types::Dimension::from(#value) })
752                }
753                "max_height" => {
754                    layout_fields.push(quote! { max_height: mirui::types::Dimension::from(#value) })
755                }
756                "grow" => {
757                    layout_fields.push(quote! { grow: mirui::types::Fixed::from_f32(#value) })
758                }
759                "shrink" => {
760                    layout_fields.push(quote! { shrink: mirui::types::Fixed::from_f32(#value) })
761                }
762                "direction" => {
763                    user_set_direction = true;
764                    layout_fields.push(quote! { direction: #value });
765                }
766                "wrap" => layout_fields.push(quote! { wrap: #value }),
767                "justify" => layout_fields.push(quote! { justify: #value }),
768                "align" => layout_fields.push(quote! { align: #value }),
769                "padding" => layout_fields.push(quote! { padding: #value }),
770                "gap" => {
771                    layout_fields.push(quote! { row_gap: mirui::types::Dimension::from(#value) });
772                    layout_fields
773                        .push(quote! { column_gap: mirui::types::Dimension::from(#value) });
774                }
775                "row_gap" => {
776                    layout_fields.push(quote! { row_gap: mirui::types::Dimension::from(#value) })
777                }
778                "column_gap" => {
779                    layout_fields.push(quote! { column_gap: mirui::types::Dimension::from(#value) })
780                }
781                "position" => layout_fields.push(quote! { position: #value }),
782                "left" => {
783                    layout_fields.push(quote! { left: mirui::types::Dimension::from(#value) })
784                }
785                "top" => layout_fields.push(quote! { top: mirui::types::Dimension::from(#value) }),
786                "image" => builder_calls.push(quote! { .image(#value) }),
787                "id" => match Self::extract_id_str(value) {
788                    Some(s) => id_registrations.push(quote! { #s }),
789                    None => errors.push(
790                        syn::Error::new_spanned(
791                            value,
792                            "id attribute must be a string literal, e.g. `id: \"submit\"`",
793                        )
794                        .to_compile_error(),
795                    ),
796                },
797                unknown => match widget_kind {
798                    WidgetKind::Component => {
799                        let field_ident = syn::Ident::new(unknown, attr_span);
800                        component_fields.push(Self::field_init_tokens(&field_ident, value));
801                    }
802                    WidgetKind::Layout | WidgetKind::IllegalLowercase => {
803                        let mut msg = format!("unknown widget attribute `{unknown}`");
804                        let candidates = LAYOUT_ATTRS
805                            .iter()
806                            .copied()
807                            .chain(STYLE_ATTRS.iter().copied())
808                            .chain(["text", "image", "id"].iter().copied());
809                        if let Some(hint) = crate::diag::closest(unknown, candidates, 2) {
810                            msg.push_str(&format!(". did you mean `{hint}`?"));
811                        }
812                        errors.push(syn::Error::new(attr_span, msg).to_compile_error());
813                    }
814                },
815            }
816        }
817        ParsedAttrs {
818            builder_calls,
819            layout_fields,
820            errors,
821            component_inserts,
822            component_fields,
823            text_tuple_value,
824            text_paragraph_value,
825            id_registrations,
826            id_lookups,
827            reactive_binds,
828            layout_binds,
829            user_set_direction,
830        }
831    }
832
833    fn extract_id_str(expr: &syn::Expr) -> Option<syn::LitStr> {
834        if let syn::Expr::Lit(syn::ExprLit {
835            lit: syn::Lit::Str(s),
836            ..
837        }) = expr
838        {
839            Some(s.clone())
840        } else {
841            None
842        }
843    }
844
845    fn field_init_tokens(field_ident: &syn::Ident, value: &syn::Expr) -> proc_macro2::TokenStream {
846        let is_float_literal = matches!(
847            value,
848            syn::Expr::Lit(syn::ExprLit {
849                lit: syn::Lit::Float(_),
850                ..
851            })
852        );
853        if is_float_literal {
854            quote! { #field_ident: #value }
855        } else {
856            quote! { #field_ident: (#value).into() }
857        }
858    }
859
860    /// Emit a Cmd, returning generated tokens.
861    /// `parent_var` is the variable name of the parent widget that children attach to.
862    fn emit_cmd(
863        cmd: &Cmd,
864        world: &proc_macro2::TokenStream,
865        parent_var: &proc_macro2::TokenStream,
866    ) -> proc_macro2::TokenStream {
867        match cmd {
868            Cmd::Widget(w) => Self::emit_widget(w, world),
869            Cmd::Compose(c) => Self::emit_compose(c, world, parent_var),
870            Cmd::Iter(i) => Self::emit_iter(i, world, parent_var),
871            Cmd::If(i) => Self::emit_if(i, world, parent_var),
872            Cmd::Niche(n) => Self::emit_niche(n, world, parent_var),
873            Cmd::Match(m) => Self::emit_match(m, world, parent_var),
874            Cmd::CodeBlock(ts) => quote! { #ts },
875        }
876    }
877
878    fn emit_compose(
879        cmd: &ComposeCmd,
880        world: &proc_macro2::TokenStream,
881        parent_var: &proc_macro2::TokenStream,
882    ) -> proc_macro2::TokenStream {
883        let function = &cmd.function;
884        let var = &cmd.var;
885        let args = &cmd.args;
886        let errors = &cmd.errors;
887        let id_lookups = cmd.id_lookups.iter().map(|(ident, key)| {
888            quote! {
889                let #ident = mirui::ecs::World::find_by_id(&*(#world), #key)
890                    .expect(concat!("ui!: id '", #key, "' not found in IdMap"));
891            }
892        });
893        quote! {
894            #(#errors)*
895            let #var: mirui::ecs::Entity = {
896                #(#id_lookups)*
897                let mut __compose_cx = mirui::ui::UiScope::new(#world, #parent_var);
898                #function(&mut __compose_cx #(, #args)*)
899            };
900        }
901    }
902
903    fn emit_on_dispatch(
904        widget_name: &str,
905        handlers: &[&OnCmd],
906        widget_var: &syn::Ident,
907        world: &proc_macro2::TokenStream,
908    ) -> proc_macro2::TokenStream {
909        let mut errors = proc_macro2::TokenStream::new();
910        let mut gesture_arms: std::collections::BTreeMap<String, Vec<&OnCmd>> =
911            std::collections::BTreeMap::new();
912        let mut business_arms: std::collections::BTreeMap<
913            &'static str,
914            Vec<(&'static BusinessEventEntry, &OnCmd)>,
915        > = std::collections::BTreeMap::new();
916
917        for h in handlers {
918            let event_name = h.name.to_string();
919
920            if let Some(q) = h.qualifier.as_ref() {
921                let q_str = q.to_string();
922                if q_str != widget_name {
923                    errors.extend(
924                        syn::Error::new(
925                            q.span(),
926                            format!(
927                                "qualified `on {q_str}::{event_name}` does not match enclosing \
928                                 widget `{widget_name}`; drop the qualifier or rename the widget"
929                            ),
930                        )
931                        .to_compile_error(),
932                    );
933                    continue;
934                }
935                match lookup_business_event(widget_name, &event_name) {
936                    Some(entry) => {
937                        business_arms
938                            .entry(entry.handler_component)
939                            .or_default()
940                            .push((entry, *h));
941                    }
942                    None => {
943                        errors.extend(
944                            syn::Error::new(
945                                h.name.span(),
946                                format!(
947                                    "no business event named `{event_name}` is registered for \
948                                     widget `{widget_name}`"
949                                ),
950                            )
951                            .to_compile_error(),
952                        );
953                    }
954                }
955                continue;
956            }
957
958            if let Some(entry) = lookup_business_event(widget_name, &event_name) {
959                if !h.args.is_empty() {
960                    errors.extend(
961                        syn::Error::new(
962                            h.name.span(),
963                            format!("business event `{event_name}` does not take arguments"),
964                        )
965                        .to_compile_error(),
966                    );
967                    continue;
968                }
969                business_arms
970                    .entry(entry.handler_component)
971                    .or_default()
972                    .push((entry, *h));
973                continue;
974            }
975
976            if !is_known_gesture_event(&event_name) {
977                errors.extend(
978                    syn::Error::new(
979                        h.name.span(),
980                        format!(
981                            "unknown event `{event_name}` for widget `{widget_name}`; \
982                             expected a GestureEvent variant (Tap, LongPress, DragStart, \
983                             DragMove, DragEnd, Pinch, Rotate) or a business event"
984                        ),
985                    )
986                    .to_compile_error(),
987                );
988                continue;
989            }
990            if !h.args.is_empty() && event_name != "Tap" {
991                errors.extend(
992                    syn::Error::new(
993                        h.name.span(),
994                        format!(
995                            "parameters on `on {event_name}(...)` are reserved for a \
996                             later release; only `on Tap(n)` is parameterised in v0.27.x"
997                        ),
998                    )
999                    .to_compile_error(),
1000                );
1001                continue;
1002            }
1003            gesture_arms.entry(event_name).or_default().push(*h);
1004        }
1005
1006        let mut tokens = proc_macro2::TokenStream::new();
1007        tokens.extend(errors);
1008
1009        if !gesture_arms.is_empty() {
1010            let mut arms = proc_macro2::TokenStream::new();
1011            for (event_name, group) in &gesture_arms {
1012                arms.extend(emit_event_arm(event_name, group));
1013            }
1014            // A closure (not a fn item) so the body can capture handles like a
1015            // Signal from the surrounding scope.
1016            tokens.extend(quote! {
1017                (#world).insert(#widget_var, ::mirui::ui::HitTarget);
1018                (#world).insert(
1019                    #widget_var,
1020                    ::mirui::input::event::GestureHandler {
1021                        on_gesture: ::mirui::input::event::GestureCallback::Closure(::mirui::__Rc::new(
1022                            move |__world: &mut ::mirui::ecs::World,
1023                                  __entity: ::mirui::ecs::Entity,
1024                                  __event: &::mirui::input::event::gesture::GestureEvent|
1025                                  -> bool {
1026                                match __event {
1027                                    #arms
1028                                    _ => false,
1029                                }
1030                            },
1031                        )),
1032                    },
1033                );
1034            });
1035        }
1036
1037        for (handler_component, group) in &business_arms {
1038            tokens.extend(emit_business_handler(
1039                widget_var,
1040                handler_component,
1041                group,
1042                world,
1043            ));
1044        }
1045
1046        tokens
1047    }
1048
1049    fn emit_widget(cmd: &WidgetCmd, world: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
1050        let var = &cmd.var;
1051        let attrs = &cmd.attrs;
1052        let layout_fields = &cmd.layout_fields;
1053        let errors = &cmd.errors;
1054
1055        let mut tokens = proc_macro2::TokenStream::new();
1056
1057        for e in errors {
1058            tokens.extend(e.clone());
1059        }
1060
1061        for (ident, key) in &cmd.id_lookups {
1062            tokens.extend(quote! {
1063                let #ident = mirui::ecs::World::find_by_id(&*(#world), #key)
1064                    .expect(concat!("ui!: id '", #key, "' not found in IdMap"));
1065            });
1066        }
1067
1068        let on_handlers: Vec<&OnCmd> = cmd.on_handlers.iter().collect();
1069        let layout_call = if layout_fields.is_empty() {
1070            quote! {}
1071        } else {
1072            quote! { .layout(mirui::ui::layout::LayoutStyle { #(#layout_fields,)* ..Default::default() }) }
1073        };
1074
1075        tokens.extend(quote! {
1076            let #var = mirui::ui::builder::WidgetBuilder::new(#world)
1077                #(#attrs)*
1078                #layout_call
1079                .id();
1080        });
1081
1082        if cmd.name == "Scroll" {
1083            tokens.extend(quote! {
1084                if let Some(__style) = (#world).get_mut::<mirui::ui::Style>(#var) {
1085                    __style.clip_children = true;
1086                }
1087                (#world).insert(#var, mirui::input::event::scroll::ScrollOffset::default());
1088                (#world).insert(
1089                    #var,
1090                    mirui::input::event::scroll::ScrollConfig {
1091                        elastic: false,
1092                        ..Default::default()
1093                    },
1094                );
1095            });
1096        }
1097
1098        // Must precede the reactive-bind injection: an effect's first run writes
1099        // into the component, so seeding it after would clobber that value.
1100        if cmd.kind == WidgetKind::Component {
1101            let comp_name = &cmd.name;
1102            if cmd.name == "Text" {
1103                let init = match (&cmd.text_tuple_value, &cmd.text_paragraph_value) {
1104                    (Some(text_value), Some(paragraph_value)) => quote! {
1105                        ::core::convert::Into::<#comp_name>::into(#text_value)
1106                            .with_paragraph(#paragraph_value)
1107                    },
1108                    (Some(text_value), None) => {
1109                        quote! { ::core::convert::Into::<#comp_name>::into(#text_value) }
1110                    }
1111                    (None, Some(paragraph_value)) => {
1112                        quote! { #comp_name::from("").with_paragraph(#paragraph_value) }
1113                    }
1114                    (None, None) => quote! { #comp_name::from("") },
1115                };
1116                tokens.extend(quote! {
1117                    (#world).insert(#var, #init);
1118                });
1119            } else {
1120                let comp_fields = &cmd.component_fields;
1121                tokens.extend(quote! {
1122                    (#world).insert(#var, {
1123                        #[allow(clippy::needless_update)]
1124                        let __c = #comp_name {
1125                            #(#comp_fields,)*
1126                            ..Default::default()
1127                        };
1128                        __c
1129                    });
1130                });
1131                if cmd.name == "Button"
1132                    && let Some(text_value) = &cmd.text_tuple_value
1133                {
1134                    tokens.extend(quote! {
1135                        (#world).insert(
1136                            #var,
1137                            ::mirui::ui::widgets::Text::label(#text_value),
1138                        );
1139                    });
1140                }
1141            }
1142
1143            tokens.extend(quote! {
1144                mirui::input::event::widget_input::attach_handlers_for(#world, #var);
1145            });
1146        }
1147
1148        if !cmd.reactive_binds.is_empty() {
1149            let injections: Vec<proc_macro2::TokenStream> = cmd
1150                .reactive_binds
1151                .iter()
1152                .map(|b| {
1153                    let property = &b.property;
1154                    let expr = &b.expr;
1155                    let value = if property == "TextContent" {
1156                        quote! { mirui::ui::property::IntoText::into_text(__v) }
1157                    } else {
1158                        quote! { ::core::convert::Into::into(__v) }
1159                    };
1160                    quote! {
1161                        mirui::core::reactive::effect_with_widget(#var, move || {
1162                            let __v = #expr;
1163                            mirui::ui::property::apply::<mirui::ui::property::prop::#property>(#var, #value);
1164                        });
1165                    }
1166                })
1167                .collect();
1168            // with_world_scope makes the effects' first run apply the initial
1169            // value now, at construction (outside the per-frame flush).
1170            tokens.extend(quote! {
1171                mirui::core::reactive::with_world_scope(#world, || { #(#injections)* });
1172            });
1173        }
1174
1175        if !cmd.layout_binds.is_empty() {
1176            let mut dependencies = Vec::new();
1177            for binding in &cmd.layout_binds {
1178                for dependency in &binding.value.dependencies {
1179                    if !dependencies
1180                        .iter()
1181                        .any(|candidate: &crate::layout_reactive::Dependency| {
1182                            candidate.same_source(dependency)
1183                        })
1184                    {
1185                        dependencies.push(dependency.clone());
1186                    }
1187                }
1188            }
1189            if dependencies.len() > 4 {
1190                tokens.extend(
1191                    syn::Error::new(
1192                        cmd.layout_binds[0].property.span(),
1193                        "layout-responsive properties on one widget use more than four distinct dependencies",
1194                    )
1195                    .to_compile_error(),
1196                );
1197            } else {
1198                let emitted_dependencies = dependencies.iter().map(|dependency| dependency.emit());
1199                let mut applies = Vec::new();
1200                for binding in &cmd.layout_binds {
1201                    let mut value = binding.value.clone();
1202                    match value.emit_apply(&binding.property, &dependencies) {
1203                        Ok(apply) => applies.push(apply),
1204                        Err(error) => tokens.extend(error.to_compile_error()),
1205                    }
1206                }
1207                tokens.extend(quote! {
1208                    (#world).insert(
1209                        #var,
1210                        ::mirui::ui::LayoutBinding::new(
1211                            const { &[#(#emitted_dependencies),*] },
1212                            |__layout_world, __layout_target, __layout_values| {
1213                                let mut __layout_changed = false;
1214                                #(#applies)*
1215                                __layout_changed
1216                            },
1217                        ),
1218                    );
1219                });
1220            }
1221        }
1222
1223        // Emit enchants — insert components
1224        let enchants = &cmd.enchants;
1225        for enchant in enchants {
1226            tokens.extend(quote! {
1227                (#world).insert(#var, #enchant);
1228            });
1229        }
1230
1231        if !on_handlers.is_empty() {
1232            tokens.extend(Self::emit_on_dispatch(
1233                &cmd.name.to_string(),
1234                &on_handlers,
1235                &cmd.var,
1236                world,
1237            ));
1238        }
1239
1240        for id_lit in &cmd.id_registrations {
1241            tokens.extend(quote! {
1242                (#world).insert(#var, mirui::ui::NamedId(#id_lit));
1243                if let Some(__map) = (#world).resource_mut::<mirui::ui::IdMap>() {
1244                    __map.insert(#id_lit, #var);
1245                }
1246            });
1247        }
1248
1249        let var_ts = quote! { #var };
1250        for child in &cmd.children {
1251            tokens.extend(Self::emit_cmd(child, world, &var_ts));
1252            if let Cmd::Widget(widget) = child {
1253                let child_var = &widget.var;
1254                tokens.extend(quote! {
1255                    {
1256                        use mirui::ui::{Children, Parent};
1257                        (#world).insert(#child_var, Parent(#var));
1258                        if let Some(children) = (#world).get_mut::<Children>(#var) {
1259                            children.0.push(#child_var);
1260                        }
1261                    }
1262                });
1263            }
1264        }
1265
1266        tokens
1267    }
1268
1269    fn emit_match(
1270        cmd: &MatchCmd,
1271        world: &proc_macro2::TokenStream,
1272        parent_var: &proc_macro2::TokenStream,
1273    ) -> proc_macro2::TokenStream {
1274        if cmd.reactive {
1275            let read = reactive_read(&cmd.scrutinee);
1276            Self::emit_match_reactive(cmd, &read, world, parent_var)
1277        } else {
1278            Self::emit_match_static(cmd, world, parent_var)
1279        }
1280    }
1281
1282    fn emit_match_reactive(
1283        cmd: &MatchCmd,
1284        read: &proc_macro2::TokenStream,
1285        world: &proc_macro2::TokenStream,
1286        parent_var: &proc_macro2::TokenStream,
1287    ) -> proc_macro2::TokenStream {
1288        let select_arms = cmd.arms.iter().map(|arm| {
1289            let pat = &arm.pat;
1290            let builder = Self::build_branch(&arm.body);
1291            quote! { #pat => (#builder)(__w, __parent_e), }
1292        });
1293        quote! {
1294            {
1295                let __mounted = mirui::__Rc::new(mirui::__Cell::new(
1296                    ::core::option::Option::<mirui::ecs::Entity>::None,
1297                ));
1298                let __parent_e = #parent_var;
1299                mirui::core::reactive::with_world_scope(#world, || {
1300                    mirui::core::reactive::effect_with_widget(__parent_e, move || {
1301                        let __sel = #read;
1302                        mirui::core::reactive::with_world(|__w| {
1303                            let __old = __mounted.take();
1304                            let __idx = __old.and_then(|o| {
1305                                __w.get::<mirui::ui::Children>(__parent_e)
1306                                    .and_then(|c| c.0.iter().position(|&e| e == o))
1307                            });
1308                            if let Some(__o) = __old {
1309                                mirui::ui::despawn_subtree(__w, __o);
1310                            }
1311                            let __root: ::core::option::Option<mirui::ecs::Entity> = match __sel {
1312                                #(#select_arms)*
1313                            };
1314                            if let Some(__r) = __root {
1315                                if let Some(children) =
1316                                    __w.get_mut::<mirui::ui::Children>(__parent_e)
1317                                {
1318                                    let __pos = __idx.unwrap_or(children.0.len()).min(children.0.len());
1319                                    children.0.insert(__pos, __r);
1320                                }
1321                            }
1322                            __mounted.set(__root);
1323                        });
1324                    });
1325                });
1326            }
1327        }
1328    }
1329
1330    fn emit_match_static(
1331        cmd: &MatchCmd,
1332        world: &proc_macro2::TokenStream,
1333        parent_var: &proc_macro2::TokenStream,
1334    ) -> proc_macro2::TokenStream {
1335        let scrutinee = &cmd.scrutinee;
1336        let arm_tokens = cmd.arms.iter().map(|arm| {
1337            let pat = &arm.pat;
1338            let mut body_tokens = proc_macro2::TokenStream::new();
1339            for child in &arm.body {
1340                body_tokens.extend(Self::emit_cmd(child, world, parent_var));
1341                if let Cmd::Widget(w) = child {
1342                    let child_var = &w.var;
1343                    body_tokens.extend(quote! {
1344                        {
1345                            use mirui::ui::{Children, Parent};
1346                            (#world).insert(#child_var, Parent(#parent_var));
1347                            if let Some(children) = (#world).get_mut::<Children>(#parent_var) {
1348                                children.0.push(#child_var);
1349                            }
1350                        }
1351                    });
1352                }
1353            }
1354            quote! { #pat => { #body_tokens } }
1355        });
1356
1357        quote! {
1358            match #scrutinee {
1359                #(#arm_tokens)*
1360            }
1361        }
1362    }
1363
1364    fn emit_niche(
1365        cmd: &NicheCmd,
1366        world: &proc_macro2::TokenStream,
1367        parent_var: &proc_macro2::TokenStream,
1368    ) -> proc_macro2::TokenStream {
1369        if cmd.is_declaration {
1370            if let Some(slot_var) = &cmd.mold_slot_var {
1371                let slot_name = cmd.name.to_string();
1372                let slot_var_ts = quote! { #slot_var };
1373                let mut fallback = proc_macro2::TokenStream::new();
1374                for child in &cmd.body {
1375                    fallback.extend(Self::emit_cmd(child, world, &slot_var_ts));
1376                    if let Cmd::Widget(w) = child {
1377                        let child_var = &w.var;
1378                        fallback.extend(quote! {
1379                            {
1380                                use mirui::ui::{Children, Parent};
1381                                (#world).insert(#child_var, Parent(#slot_var_ts));
1382                                if let Some(children) = (#world).get_mut::<Children>(#slot_var_ts) {
1383                                    children.0.push(#child_var);
1384                                }
1385                            }
1386                        });
1387                    }
1388                }
1389                return quote! {
1390                    let __slot_parent = #parent_var;
1391                    let #slot_var = ::mirui::ui::builder::WidgetBuilder::new(#world).id();
1392                    (#world).insert(#slot_var, ::mirui::ui::Parent(__slot_parent));
1393                    if let Some(children) = (#world).get_mut::<::mirui::ui::Children>(__slot_parent) {
1394                        children.0.push(#slot_var);
1395                    } else {
1396                        (#world).insert(__slot_parent, ::mirui::ui::Children(::mirui::__Vec::from([#slot_var])));
1397                    }
1398                    __mold_niche_map.insert(#slot_name, #slot_var);
1399                    #fallback
1400                };
1401            }
1402            let msg = format!(
1403                "ui!: `@@{}` is a slot declaration; use it inside `mold!` bodies, not `ui!` — call sites fill slots with the single-`@` form `@{}` {{ ... }}",
1404                cmd.name, cmd.name,
1405            );
1406            return quote! {
1407                compile_error!(#msg);
1408            };
1409        }
1410
1411        let niche_name = cmd.name.to_string();
1412        let niche_var = syn::Ident::new(
1413            &format!("__niche_{}", cmd.name),
1414            proc_macro2::Span::call_site(),
1415        );
1416        let niche_var_ts = quote! { #niche_var };
1417
1418        let slot_check = if let Some(pw) = &cmd.parent_widget {
1419            let slot_method = syn::Ident::new(&format!("__slot_{}", cmd.name), cmd.name.span());
1420            quote! {
1421                let _ = <#pw>::#slot_method;
1422            }
1423        } else {
1424            quote! {}
1425        };
1426
1427        let mut body_tokens = proc_macro2::TokenStream::new();
1428        for child in &cmd.body {
1429            body_tokens.extend(Self::emit_cmd(child, world, &niche_var_ts));
1430            if let Cmd::Widget(w) = child {
1431                let child_var = &w.var;
1432                body_tokens.extend(quote! {
1433                    {
1434                        use mirui::ui::{Children, Parent};
1435                        (#world).insert(#child_var, Parent(#niche_var_ts));
1436                        if let Some(children) = (#world).get_mut::<Children>(#niche_var_ts) {
1437                            children.0.push(#child_var);
1438                        }
1439                    }
1440                });
1441            }
1442        }
1443
1444        let widget_label = quote! { stringify!(#parent_var) };
1445        quote! {
1446            #slot_check
1447            let #niche_var = match (#world).get::<mirui::ui::NicheMap>(#parent_var) {
1448                Some(map) => match map.get(#niche_name) {
1449                    Some(e) => e,
1450                    None => panic!(
1451                        "ui!: niche '{}' not registered on widget {}",
1452                        #niche_name,
1453                        #widget_label,
1454                    ),
1455                },
1456                None => panic!(
1457                    "ui!: widget {} has no NicheMap (missing auto_attach?)",
1458                    #widget_label
1459                ),
1460            };
1461            {
1462                let __existing: ::mirui::__Vec<::mirui::ecs::Entity> = (#world)
1463                    .get::<::mirui::ui::Children>(#niche_var_ts)
1464                    .map(|c| c.0.clone())
1465                    .unwrap_or_default();
1466                for __e in __existing {
1467                    ::mirui::ui::despawn_subtree(#world, __e);
1468                }
1469            }
1470            #body_tokens
1471        }
1472    }
1473
1474    fn emit_iter(
1475        cmd: &IterCmd,
1476        world: &proc_macro2::TokenStream,
1477        parent_var: &proc_macro2::TokenStream,
1478    ) -> proc_macro2::TokenStream {
1479        if cmd.reactive {
1480            Self::emit_iter_reactive(cmd, world, parent_var)
1481        } else {
1482            Self::emit_iter_static(cmd, world, parent_var)
1483        }
1484    }
1485
1486    fn emit_iter_static(
1487        cmd: &IterCmd,
1488        world: &proc_macro2::TokenStream,
1489        parent_var: &proc_macro2::TokenStream,
1490    ) -> proc_macro2::TokenStream {
1491        let iterable = &cmd.iterable;
1492        let variable = &cmd.variable;
1493
1494        let mut body_tokens = proc_macro2::TokenStream::new();
1495        for child in &cmd.body {
1496            body_tokens.extend(Self::emit_cmd(child, world, parent_var));
1497            if let Cmd::Widget(w) = child {
1498                let child_var = &w.var;
1499                body_tokens.extend(quote! {
1500                    {
1501                        use mirui::ui::{Children, Parent};
1502                        (#world).insert(#child_var, Parent(#parent_var));
1503                        if let Some(children) = (#world).get_mut::<Children>(#parent_var) {
1504                            children.0.push(#child_var);
1505                        }
1506                    }
1507                });
1508            }
1509        }
1510
1511        quote! {
1512            for #variable in #iterable {
1513                #body_tokens
1514            }
1515        }
1516    }
1517
1518    fn emit_if(
1519        cmd: &IfCmd,
1520        world: &proc_macro2::TokenStream,
1521        parent_var: &proc_macro2::TokenStream,
1522    ) -> proc_macro2::TokenStream {
1523        if cmd.reactive {
1524            Self::emit_if_reactive(cmd, world, parent_var)
1525        } else {
1526            Self::emit_if_static(cmd, world, parent_var)
1527        }
1528    }
1529
1530    fn emit_branch_body_inline(
1531        body: &[Cmd],
1532        world: &proc_macro2::TokenStream,
1533        parent_var: &proc_macro2::TokenStream,
1534    ) -> proc_macro2::TokenStream {
1535        let mut body_tokens = proc_macro2::TokenStream::new();
1536        for child in body {
1537            body_tokens.extend(Self::emit_cmd(child, world, parent_var));
1538            if let Cmd::Widget(w) = child {
1539                let child_var = &w.var;
1540                body_tokens.extend(quote! {
1541                    {
1542                        use mirui::ui::{Children, Parent};
1543                        (#world).insert(#child_var, Parent(#parent_var));
1544                        if let Some(children) = (#world).get_mut::<Children>(#parent_var) {
1545                            children.0.push(#child_var);
1546                        }
1547                    }
1548                });
1549            }
1550        }
1551        body_tokens
1552    }
1553
1554    fn emit_if_static(
1555        cmd: &IfCmd,
1556        world: &proc_macro2::TokenStream,
1557        parent_var: &proc_macro2::TokenStream,
1558    ) -> proc_macro2::TokenStream {
1559        let mut chain = proc_macro2::TokenStream::new();
1560        for (i, br) in cmd.branches.iter().enumerate() {
1561            let body = Self::emit_branch_body_inline(&br.body, world, parent_var);
1562            match (&br.cond, i) {
1563                (Some(c), 0) => chain.extend(quote! { if #c { #body } }),
1564                (Some(c), _) => chain.extend(quote! { else if #c { #body } }),
1565                (None, _) => chain.extend(quote! { else { #body } }),
1566            }
1567        }
1568        chain
1569    }
1570
1571    fn collect_children(&mut self, children: &[DsTreeRef]) -> Vec<Cmd> {
1572        self.stack.push(Vec::new());
1573        for child in children {
1574            decipher(child, self);
1575        }
1576        self.stack.pop().unwrap()
1577    }
1578
1579    // A reactive control-flow branch built on demand: returns the first top
1580    // widget (single-root) as the mount root, parented but NOT pushed into the
1581    // parent's Children — the effect inserts it positionally so a branch swap
1582    // keeps its index. Non-root top widgets push normally.
1583    fn branch_body(body: &[Cmd]) -> proc_macro2::TokenStream {
1584        let w = quote! { __w };
1585        let p = quote! { __parent };
1586        let mut stmts = proc_macro2::TokenStream::new();
1587        let mut root: Option<proc_macro2::TokenStream> = None;
1588        for child in body {
1589            stmts.extend(Self::emit_cmd(child, &w, &p));
1590            let child_var = match child {
1591                Cmd::Widget(cw) => Some((&cw.var, false)),
1592                Cmd::Compose(compose) => Some((&compose.var, true)),
1593                _ => None,
1594            };
1595            if let Some((cv, already_attached)) = child_var {
1596                if root.is_none() {
1597                    let detach = already_attached.then(|| {
1598                        quote! {
1599                            if let Some(children) = (#w).get_mut::<mirui::ui::Children>(#p) {
1600                                if let Some(index) = children.0.iter().position(|entity| *entity == #cv) {
1601                                    children.0.remove(index);
1602                                }
1603                            }
1604                        }
1605                    });
1606                    stmts.extend(quote! {
1607                        #detach
1608                        (#w).insert(#cv, mirui::ui::Parent(#p));
1609                    });
1610                    root = Some(quote! { #cv });
1611                } else if !already_attached {
1612                    stmts.extend(quote! {
1613                        {
1614                            use mirui::ui::{Children, Parent};
1615                            (#w).insert(#cv, Parent(#p));
1616                            if let Some(children) = (#w).get_mut::<Children>(#p) {
1617                                children.0.push(#cv);
1618                            }
1619                        }
1620                    });
1621                }
1622            }
1623        }
1624        let ret = match root {
1625            Some(v) => quote! { Some(#v) },
1626            None => quote! { None },
1627        };
1628        quote! { #stmts #ret }
1629    }
1630
1631    fn build_branch(body: &[Cmd]) -> proc_macro2::TokenStream {
1632        let inner = Self::branch_body(body);
1633        quote! {
1634            |__w: &mut mirui::ecs::World, __parent: mirui::ecs::Entity|
1635                -> Option<mirui::ecs::Entity> {
1636                #inner
1637            }
1638        }
1639    }
1640
1641    fn emit_if_reactive(
1642        cmd: &IfCmd,
1643        world: &proc_macro2::TokenStream,
1644        parent_var: &proc_macro2::TokenStream,
1645    ) -> proc_macro2::TokenStream {
1646        let builder_idents: Vec<syn::Ident> = (0..cmd.branches.len())
1647            .map(|i| syn::Ident::new(&format!("__br{i}"), proc_macro2::Span::call_site()))
1648            .collect();
1649        let let_builders = cmd.branches.iter().zip(&builder_idents).map(|(b, id)| {
1650            let builder = Self::build_branch(&b.body);
1651            quote! { let #id = #builder; }
1652        });
1653
1654        let mut cascade = proc_macro2::TokenStream::new();
1655        let mut first = true;
1656        let mut has_else = false;
1657        for (br, id) in cmd.branches.iter().zip(&builder_idents) {
1658            match &br.cond {
1659                Some(cond) => {
1660                    let read = reactive_read(cond);
1661                    let head = if first { quote!(if) } else { quote!(else if) };
1662                    cascade.extend(quote! { #head (#read) { (#id)(__w, __parent_e) } });
1663                    first = false;
1664                }
1665                None => {
1666                    cascade.extend(quote! { else { (#id)(__w, __parent_e) } });
1667                    has_else = true;
1668                }
1669            }
1670        }
1671        if !has_else {
1672            cascade.extend(quote! { else { ::core::option::Option::None } });
1673        }
1674
1675        quote! {
1676            {
1677                let __mounted = mirui::__Rc::new(mirui::__Cell::new(
1678                    ::core::option::Option::<mirui::ecs::Entity>::None,
1679                ));
1680                let __parent_e = #parent_var;
1681                #(#let_builders)*
1682                mirui::core::reactive::with_world_scope(#world, || {
1683                    mirui::core::reactive::effect_with_widget(__parent_e, move || {
1684                        mirui::core::reactive::with_world(|__w| {
1685                            let __old = __mounted.take();
1686                            let __idx = __old.and_then(|o| {
1687                                __w.get::<mirui::ui::Children>(__parent_e)
1688                                    .and_then(|c| c.0.iter().position(|&e| e == o))
1689                            });
1690                            if let Some(__o) = __old {
1691                                mirui::ui::despawn_subtree(__w, __o);
1692                            }
1693                            let __root: ::core::option::Option<mirui::ecs::Entity> = #cascade;
1694                            if let Some(__r) = __root {
1695                                if let Some(children) =
1696                                    __w.get_mut::<mirui::ui::Children>(__parent_e)
1697                                {
1698                                    let __pos = __idx.unwrap_or(children.0.len()).min(children.0.len());
1699                                    children.0.insert(__pos, __r);
1700                                }
1701                            }
1702                            __mounted.set(__root);
1703                        });
1704                    });
1705                });
1706            }
1707        }
1708    }
1709
1710    fn emit_iter_reactive(
1711        cmd: &IterCmd,
1712        world: &proc_macro2::TokenStream,
1713        parent_var: &proc_macro2::TokenStream,
1714    ) -> proc_macro2::TokenStream {
1715        if cmd.key.is_some() {
1716            Self::emit_iter_reactive_keyed(cmd, world, parent_var)
1717        } else {
1718            Self::emit_iter_reactive_indexed(cmd, world, parent_var)
1719        }
1720    }
1721
1722    fn emit_iter_reactive_indexed(
1723        cmd: &IterCmd,
1724        world: &proc_macro2::TokenStream,
1725        parent_var: &proc_macro2::TokenStream,
1726    ) -> proc_macro2::TokenStream {
1727        let iterable = &cmd.iterable;
1728        let variable = &cmd.variable;
1729        let row_body = Self::branch_body(&cmd.body);
1730
1731        quote! {
1732            {
1733                let __rows = mirui::__Rc::new(mirui::__RefCell::new(
1734                    mirui::__Vec::<mirui::ecs::Entity>::new(),
1735                ));
1736                let __parent_e = #parent_var;
1737                mirui::core::reactive::with_world_scope(#world, || {
1738                    mirui::core::reactive::effect_with_widget(__parent_e, move || {
1739                        let __items: mirui::__Vec<_> = (#iterable).into_iter().collect();
1740                        mirui::core::reactive::with_world(|__w| {
1741                            let __n_new = __items.len();
1742                            let __n_old = __rows.borrow().len();
1743                            if __n_new < __n_old {
1744                                for __e in __rows.borrow_mut().drain(__n_new..) {
1745                                    let __pos = __w
1746                                        .get::<mirui::ui::Children>(__parent_e)
1747                                        .and_then(|c| c.0.iter().position(|&x| x == __e));
1748                                    if let Some(__p) = __pos {
1749                                        if let Some(children) =
1750                                            __w.get_mut::<mirui::ui::Children>(__parent_e)
1751                                        {
1752                                            children.0.remove(__p);
1753                                        }
1754                                    }
1755                                    mirui::ui::despawn_subtree(__w, __e);
1756                                }
1757                            }
1758                            for #variable in __items.into_iter().skip(__n_old) {
1759                                let __r = (|__w: &mut mirui::ecs::World, __parent: mirui::ecs::Entity| -> Option<mirui::ecs::Entity> {
1760                                    #row_body
1761                                })(__w, __parent_e);
1762                                if let Some(__r) = __r {
1763                                    if let Some(children) =
1764                                        __w.get_mut::<mirui::ui::Children>(__parent_e)
1765                                    {
1766                                        children.0.push(__r);
1767                                    }
1768                                    __rows.borrow_mut().push(__r);
1769                                }
1770                            }
1771                        });
1772                    });
1773                });
1774            }
1775        }
1776    }
1777
1778    fn emit_iter_reactive_keyed(
1779        cmd: &IterCmd,
1780        world: &proc_macro2::TokenStream,
1781        parent_var: &proc_macro2::TokenStream,
1782    ) -> proc_macro2::TokenStream {
1783        let iterable = &cmd.iterable;
1784        let variable = &cmd.variable;
1785        let key_expr = cmd.key.as_ref().unwrap();
1786        let row_body = Self::branch_body(&cmd.body);
1787
1788        quote! {
1789            {
1790                let __rows = mirui::__Rc::new(mirui::__RefCell::new(mirui::__Vec::new()));
1791                let __parent_e = #parent_var;
1792                mirui::core::reactive::with_world_scope(#world, || {
1793                    mirui::core::reactive::effect_with_widget(__parent_e, move || {
1794                        let __items: mirui::__Vec<_> = (#iterable).into_iter().collect();
1795                        mirui::core::reactive::with_world(|__w| {
1796                            let mut __old: mirui::__Vec<_> = __rows.borrow_mut().drain(..).collect();
1797                            let mut __new = mirui::__Vec::new();
1798                            let mut __kept: mirui::__Vec<mirui::ecs::Entity> = mirui::__Vec::new();
1799                            for #variable in __items {
1800                                let __k = #key_expr;
1801                                if let Some(__i) = __old.iter().position(|(__ok, _)| *__ok == __k) {
1802                                    let (_, __e) = __old.remove(__i);
1803                                    __kept.push(__e);
1804                                    __new.push((__k, __e));
1805                                } else {
1806                                    let __r = (|__w: &mut mirui::ecs::World, __parent: mirui::ecs::Entity| -> Option<mirui::ecs::Entity> {
1807                                        #row_body
1808                                    })(__w, __parent_e);
1809                                    if let Some(__r) = __r {
1810                                        __kept.push(__r);
1811                                        __new.push((__k, __r));
1812                                    }
1813                                }
1814                            }
1815                            for (_, __e) in __old.drain(..) {
1816                                mirui::ui::despawn_subtree(__w, __e);
1817                            }
1818                            // retain-then-append reorders the rows to match the new
1819                            // key order while leaving any static siblings in place.
1820                            if let Some(children) = __w.get_mut::<mirui::ui::Children>(__parent_e) {
1821                                children.0.retain(|e| !__kept.contains(e));
1822                                for __e in &__kept {
1823                                    children.0.push(*__e);
1824                                }
1825                            }
1826                            *__rows.borrow_mut() = __new;
1827                        });
1828                    });
1829                });
1830            }
1831        }
1832    }
1833}
1834
1835impl DsRune for MiruiRune {
1836    fn inscribe_root(&mut self, _parent_expr: &syn::Expr) {}
1837
1838    fn inscribe_widget(
1839        &mut self,
1840        name: &syn::Ident,
1841        attrs: &[DsAttr],
1842        enchants: &[syn::Expr],
1843        on_handlers: &[xrune::ds_node::ds_on::DsOn],
1844        children: &[DsTreeRef],
1845    ) {
1846        let kind = classify_widget_name(name);
1847        let is_compose_call = kind == WidgetKind::IllegalLowercase
1848            && attrs.iter().all(|attr| attr.name.is_none())
1849            && enchants.is_empty()
1850            && on_handlers.is_empty()
1851            && children.is_empty();
1852        if is_compose_call {
1853            let mut args = Vec::new();
1854            let mut errors = Vec::new();
1855            let mut id_lookups = Vec::new();
1856            for attr in attrs {
1857                if let Some(attr_name) = &attr.name {
1858                    errors.push(
1859                        syn::Error::new(
1860                            attr_name.span(),
1861                            "compose calls accept positional arguments only",
1862                        )
1863                        .to_compile_error(),
1864                    );
1865                    continue;
1866                }
1867                if attr.reactive {
1868                    errors.push(
1869                        syn::Error::new(
1870                            syn::spanned::Spanned::span(&attr.value),
1871                            "compose call arguments cannot use reactive binding syntax",
1872                        )
1873                        .to_compile_error(),
1874                    );
1875                    continue;
1876                }
1877                let mut value = attr.value.clone();
1878                syn::visit_mut::VisitMut::visit_expr_mut(
1879                    &mut crate::visit_id::IdRewriter {
1880                        captured: &mut id_lookups,
1881                    },
1882                    &mut value,
1883                );
1884                args.push(value);
1885            }
1886            let var = self.next_var();
1887            self.stack
1888                .last_mut()
1889                .unwrap()
1890                .push(Cmd::Compose(ComposeCmd {
1891                    function: name.clone(),
1892                    var,
1893                    args,
1894                    errors,
1895                    id_lookups,
1896                }));
1897            return;
1898        }
1899        let var = self.next_var();
1900        let mut parsed = self.parse_attrs(attrs, &name.to_string(), kind);
1901        if !parsed.user_set_direction {
1902            let widget_name_str = name.to_string();
1903            match widget_name_str.as_str() {
1904                "Row" => {
1905                    parsed.layout_fields.insert(
1906                        0,
1907                        quote! { direction: mirui::ui::layout::FlexDirection::Row },
1908                    );
1909                }
1910                "Column" | "Scroll" => {
1911                    parsed.layout_fields.insert(
1912                        0,
1913                        quote! { direction: mirui::ui::layout::FlexDirection::Column },
1914                    );
1915                }
1916                _ => {}
1917            }
1918        }
1919        let mut id_lookups = parsed.id_lookups;
1920        let mut enchant_tokens: Vec<proc_macro2::TokenStream> = parsed.component_inserts;
1921        for e in enchants.iter() {
1922            let mut rewritten = e.clone();
1923            syn::visit_mut::VisitMut::visit_expr_mut(
1924                &mut crate::visit_id::IdRewriter {
1925                    captured: &mut id_lookups,
1926                },
1927                &mut rewritten,
1928            );
1929            enchant_tokens.push(quote! { #rewritten });
1930        }
1931
1932        let collected_on_handlers: Vec<OnCmd> = on_handlers
1933            .iter()
1934            .map(|h| {
1935                let mut args = h.get_args().to_vec();
1936                let body = match h.get_body() {
1937                    Some(b) => b.clone(),
1938                    None => {
1939                        let callback = args
1940                            .pop()
1941                            .expect("DsOn parser already rejects empty args+no-body forms");
1942                        OnCmd::synthesised_body_from_callback(&callback)
1943                    }
1944                };
1945                OnCmd {
1946                    qualifier: h.get_qualifier().cloned(),
1947                    name: h.get_name().clone(),
1948                    args,
1949                    body,
1950                }
1951            })
1952            .collect();
1953
1954        self.stack.push(Vec::new());
1955        let pushed_widget_kind = kind == WidgetKind::Component;
1956        if pushed_widget_kind {
1957            self.widget_kind_stack.push(Some(name.clone()));
1958        }
1959        for child in children {
1960            decipher(child, self);
1961        }
1962        if pushed_widget_kind {
1963            self.widget_kind_stack.pop();
1964        }
1965        let my_children = self.stack.pop().unwrap();
1966
1967        let cmd = Cmd::Widget(WidgetCmd {
1968            name: name.clone(),
1969            kind,
1970            var,
1971            attrs: parsed.builder_calls,
1972            layout_fields: parsed.layout_fields,
1973            errors: parsed.errors,
1974            enchants: enchant_tokens,
1975            on_handlers: collected_on_handlers,
1976            component_fields: parsed.component_fields,
1977            text_tuple_value: parsed.text_tuple_value,
1978            text_paragraph_value: parsed.text_paragraph_value,
1979            id_registrations: parsed.id_registrations,
1980            id_lookups,
1981            reactive_binds: parsed.reactive_binds,
1982            layout_binds: parsed.layout_binds,
1983            children: my_children,
1984        });
1985
1986        self.stack.last_mut().unwrap().push(cmd);
1987    }
1988
1989    fn inscribe_if(
1990        &mut self,
1991        condition: &syn::Expr,
1992        reactive: bool,
1993        children: &[DsTreeRef],
1994        else_branch: Option<&DsTreeRef>,
1995    ) {
1996        use xrune::ds_node::node_enum::DsNode;
1997
1998        let mut branches = vec![Branch {
1999            cond: Some(condition.clone()),
2000            body: self.collect_children(children),
2001        }];
2002
2003        let mut next = else_branch.cloned();
2004        while let Some(tree) = next {
2005            let node_children = tree.borrow().get_children().to_vec();
2006            let chain_next = tree.borrow().get_else_branch().cloned();
2007            let cond = match tree.borrow().get_node() {
2008                DsNode::If(if_node) => Some(if_node.get_condition().clone()),
2009                _ => None,
2010            };
2011            branches.push(Branch {
2012                cond,
2013                body: self.collect_children(&node_children),
2014            });
2015            next = chain_next;
2016        }
2017
2018        let cmd = Cmd::If(IfCmd { branches, reactive });
2019        self.stack.last_mut().unwrap().push(cmd);
2020    }
2021
2022    fn inscribe_iter(
2023        &mut self,
2024        iterable: &syn::Expr,
2025        variable: &syn::Ident,
2026        reactive: bool,
2027        key: Option<&syn::Expr>,
2028        children: &[DsTreeRef],
2029    ) {
2030        self.stack.push(Vec::new());
2031        for child in children {
2032            decipher(child, self);
2033        }
2034        let body = self.stack.pop().unwrap();
2035
2036        let cmd = Cmd::Iter(IterCmd {
2037            iterable: quote! { #iterable },
2038            variable: variable.clone(),
2039            body,
2040            reactive,
2041            key: key.map(|k| quote! { #k }),
2042        });
2043
2044        self.stack.last_mut().unwrap().push(cmd);
2045    }
2046
2047    fn inscribe_niche(&mut self, name: &syn::Ident, is_declaration: bool, children: &[DsTreeRef]) {
2048        let parent_widget = self.widget_kind_stack.last().and_then(|w| w.clone());
2049
2050        self.stack.push(Vec::new());
2051        for child in children {
2052            decipher(child, self);
2053        }
2054        let body = self.stack.pop().unwrap();
2055
2056        let mold_slot_var = if is_declaration && self.mold_mode {
2057            Some(syn::Ident::new(
2058                &format!("__mold_slot_{}", name),
2059                name.span(),
2060            ))
2061        } else {
2062            None
2063        };
2064
2065        let cmd = Cmd::Niche(NicheCmd {
2066            name: name.clone(),
2067            is_declaration,
2068            mold_slot_var,
2069            parent_widget,
2070            body,
2071        });
2072        self.stack.last_mut().unwrap().push(cmd);
2073    }
2074
2075    fn inscribe_code_block(&mut self, tokens: &proc_macro2::TokenStream) {
2076        self.stack
2077            .last_mut()
2078            .unwrap()
2079            .push(Cmd::CodeBlock(tokens.clone()));
2080    }
2081
2082    fn inscribe_match(
2083        &mut self,
2084        scrutinee: &syn::Expr,
2085        reactive: bool,
2086        arms: &[xrune::ds_node::ds_match::DsMatchArm],
2087    ) {
2088        let mut arm_cmds = Vec::with_capacity(arms.len());
2089        for arm in arms {
2090            self.stack.push(Vec::new());
2091            for child in arm.get_children() {
2092                decipher(child, self);
2093            }
2094            let body = self.stack.pop().unwrap();
2095            arm_cmds.push(MatchArm {
2096                pat: arm.get_pat().clone(),
2097                body,
2098            });
2099        }
2100
2101        let cmd = Cmd::Match(MatchCmd {
2102            scrutinee: scrutinee.clone(),
2103            reactive,
2104            arms: arm_cmds,
2105        });
2106        self.stack.last_mut().unwrap().push(cmd);
2107    }
2108
2109    fn seal(self) -> proc_macro2::TokenStream {
2110        let world = &self.world_expr;
2111        let parent_entity = &self.parent_expr;
2112        let mut tokens = proc_macro2::TokenStream::new();
2113
2114        let root_cmds = &self.stack[0];
2115        for cmd in root_cmds {
2116            tokens.extend(Self::emit_cmd(cmd, world, parent_entity));
2117        }
2118
2119        // Attach top-level widgets to parent (iter attaches inside its own loop)
2120        let mut last_var = None;
2121        for cmd in root_cmds {
2122            match cmd {
2123                Cmd::Widget(w) => {
2124                    let var = &w.var;
2125                    last_var = Some(var.clone());
2126                    tokens.extend(quote! {
2127                        {
2128                            use mirui::ui::{Children, Parent};
2129                            let __seal_parent = #parent_entity;
2130                            (#world).insert(#var, Parent(__seal_parent));
2131                            if let Some(children) = (#world).get_mut::<Children>(__seal_parent) {
2132                                children.0.push(#var);
2133                            }
2134                        }
2135                    });
2136                }
2137                Cmd::Compose(compose) => last_var = Some(compose.var.clone()),
2138                _ => {}
2139            }
2140        }
2141
2142        // Return the top-level widget entity
2143        if let Some(var) = last_var {
2144            quote! { { #tokens #var } }
2145        } else {
2146            quote! { { #tokens } }
2147        }
2148    }
2149}
2150
2151/// Inject a `cx: &mut UiScope` first parameter into the annotated function.
2152///
2153/// The attribute exists to keep infrastructure out of the signature the user
2154/// reads:
2155///
2156/// ```ignore
2157/// #[compose]
2158/// fn counter_button(sig: Signal<i32>) {
2159///     ui! {
2160///         View (bg_color: ColorToken::Primary)
2161///         on Tap { sig.update(|n| *n += 1); }
2162///         { Text($sig) }
2163///     };
2164/// }
2165/// ```
2166///
2167/// Every `ui!` invocation inside the function body reads `cx.world_mut()` /
2168/// `cx.parent()` implicitly; callers pass their existing `cx` through the
2169/// paired `ui!(func(args))` form so no site ever spells out `world` and
2170/// `parent` again.
2171///
2172/// The attribute rejects three shapes at expansion time:
2173///
2174/// - non-function items — annotate a fn, or reach for `mold!(Name { ... })`
2175///   when you want a full widget class.
2176/// - `async fn` — cross-await UI state is a separate topic (v0.41+).
2177/// - generic fn — parameter monomorphisation is deferred to a later release.
2178///
2179/// It also errors out if the user already declared a parameter named `cx`,
2180/// so double-injection can't silently pass through.
2181#[proc_macro_attribute]
2182pub fn compose(attr: TokenStream, item: TokenStream) -> TokenStream {
2183    compose_attr::expand(attr.into(), item.into()).into()
2184}
2185
2186/// Spawn a widget tree, or forward to another `#[compose]` function.
2187///
2188/// The macro accepts three forms, distinguished by the shape of the input:
2189///
2190/// - **Body form** (`ui! { <tree> }`) reads `cx` from the enclosing scope
2191///   and spawns the DSL tree there:
2192///
2193///   ```ignore
2194///   #[compose]
2195///   fn build_root() {
2196///       ui! {
2197///           Column (grow: 1.0) {
2198///               Text("hello")
2199///           }
2200///       };
2201///   }
2202///   ```
2203///
2204///   Lowercase `#[compose]` functions can appear directly as tree nodes. Their
2205///   returned root entity is attached at that position:
2206///
2207///   ```ignore
2208///   ui! {
2209///       Column (grow: 1.0) {
2210///           compose_header()
2211///           compose_card("Status")
2212///       }
2213///   };
2214///   ```
2215///
2216///   `world` and `parent` come from `cx.world_mut()` / `cx.parent()` by
2217///   default. The legacy header form `ui! { :( parent world :) X }` still
2218///   parses and takes precedence over the implicit `cx` lookup — useful
2219///   for tests, one-off snapshots, or any code path that isn't inside an
2220///   `#[compose]` fn.
2221///
2222/// - **Fn-call form** (`ui!(func(args))`) rewrites a free-fn call to
2223///   `func(cx, args)`, threading the enclosing scope's `cx` into the callee
2224///   when no surrounding DSL node supplies the parent:
2225///
2226///   ```ignore
2227///   #[compose]
2228///   fn menu() {
2229///       ui!(menu_row("File", &FILE_ICON));
2230///       ui!(menu_row("Edit", &EDIT_ICON));
2231///   }
2232///   ```
2233///
2234///   The macro routes call-syntax input to this form only when the fn
2235///   path segment starts with a lowercase letter or underscore. Names
2236///   that start uppercase (`Card(...)`, `Column(...)`) stay on the DSL
2237///   parsing path — the same convention the widget grammar uses.
2238#[proc_macro]
2239pub fn ui(input: TokenStream) -> TokenStream {
2240    let input2: proc_macro2::TokenStream = input.into();
2241
2242    if let Some(after) = try_strip_compose_keyword(&input2) {
2243        return mold::expand(after).into();
2244    }
2245
2246    if let Some(call) = try_parse_fn_call_form(&input2) {
2247        return expand_ui_fn_call(call).into();
2248    }
2249
2250    let input2 = match crate::layout_reactive::preprocess(input2) {
2251        Ok(input) => input,
2252        Err(error) => return error.to_compile_error().into(),
2253    };
2254    let root = match syn::parse2::<DsRoot>(input2) {
2255        Ok(r) => r,
2256        Err(e) => return e.to_compile_error().into(),
2257    };
2258    let mut rune = MiruiRune::new();
2259
2260    let context_attrs = root.get_context_attrs();
2261    let world_override = context_attrs
2262        .iter()
2263        .find(|a| a.name.as_ref().is_some_and(|n| n == "world"))
2264        .map(|a| {
2265            let v = &a.value;
2266            quote! { #v }
2267        });
2268    let parent_override = context_attrs
2269        .iter()
2270        .find(|a| a.name.as_ref().is_some_and(|n| n == "parent"))
2271        .map(|a| {
2272            let v = &a.value;
2273            quote! { #v }
2274        });
2275
2276    rune.world_expr = world_override
2277        .clone()
2278        .unwrap_or_else(|| quote! { cx.world_mut() });
2279    rune.parent_expr = quote! { __mirui_parent };
2280
2281    rune.inscribe_root(&root.get_parent());
2282    let content = root.get_content();
2283    decipher(&content, &mut rune);
2284    let body = rune.seal();
2285
2286    let parent_bind = match parent_override {
2287        Some(expr) => quote! { let __mirui_parent: ::mirui::ecs::Entity = #expr; },
2288        None => quote! { let __mirui_parent: ::mirui::ecs::Entity = cx.parent(); },
2289    };
2290
2291    TokenStream::from(quote! {
2292        {
2293            #parent_bind
2294            #body
2295        }
2296    })
2297}
2298
2299fn try_strip_compose_keyword(input: &proc_macro2::TokenStream) -> Option<proc_macro2::TokenStream> {
2300    let mut iter = input.clone().into_iter();
2301    let first = iter.next()?;
2302    let ident = match first {
2303        proc_macro2::TokenTree::Ident(i) if i == "compose" => i,
2304        _ => return None,
2305    };
2306    let _ = ident;
2307    Some(iter.collect())
2308}
2309
2310fn try_parse_fn_call_form(input: &proc_macro2::TokenStream) -> Option<syn::ExprCall> {
2311    let call: syn::ExprCall = syn::parse2(input.clone()).ok()?;
2312    let path = match &*call.func {
2313        syn::Expr::Path(p) => p,
2314        _ => return None,
2315    };
2316    let last = path.path.segments.last()?;
2317    let name = last.ident.to_string();
2318    let first_char = name.chars().next()?;
2319    if first_char.is_ascii_lowercase() || first_char == '_' {
2320        Some(call)
2321    } else {
2322        None
2323    }
2324}
2325
2326fn expand_ui_fn_call(call: syn::ExprCall) -> proc_macro2::TokenStream {
2327    let func = &call.func;
2328    let args = &call.args;
2329    quote! {
2330        #func(cx, #args)
2331    }
2332}
2333
2334#[proc_macro]
2335pub fn compose_backend(input: TokenStream) -> TokenStream {
2336    compose::expand(input.into()).into()
2337}
2338
2339#[proc_macro]
2340pub fn path(input: TokenStream) -> TokenStream {
2341    vector::expand_path(input.into()).into()
2342}
2343
2344#[proc_macro]
2345pub fn scene(input: TokenStream) -> TokenStream {
2346    vector::expand_scene(input.into()).into()
2347}
2348
2349/// ```rust,ignore
2350/// timer!(Cycle, every: 3_000, |world, entity| { /* ... */ });
2351/// // schedule: after: ms | every: ms | repeat: N every: ms | until: D every: ms
2352/// ```
2353///
2354/// Sugar over `Timer::after / every / repeat / until`; all four share
2355/// the generic `timer_system`, so N invocations don't grow the binary.
2356#[proc_macro]
2357pub fn timer(input: TokenStream) -> TokenStream {
2358    timer_impl::expand(input.into()).into()
2359}
2360
2361mod timer_impl {
2362    use proc_macro2::TokenStream;
2363    use quote::quote;
2364    use syn::parse::{Parse, ParseStream};
2365    use syn::{ExprClosure, Ident, LitInt, Token, parse2};
2366
2367    enum Schedule {
2368        After(LitInt),
2369        Every(LitInt),
2370        Repeat { times: LitInt, period: LitInt },
2371        Until { deadline: LitInt, period: LitInt },
2372    }
2373
2374    struct TimerInput {
2375        name: Ident,
2376        schedule: Schedule,
2377        closure: ExprClosure,
2378    }
2379
2380    impl Parse for Schedule {
2381        fn parse(input: ParseStream) -> syn::Result<Self> {
2382            let kind: Ident = input.parse()?;
2383            input.parse::<Token![:]>()?;
2384            let first: LitInt = input.parse()?;
2385            match kind.to_string().as_str() {
2386                "after" => Ok(Schedule::After(first)),
2387                "every" => Ok(Schedule::Every(first)),
2388                "repeat" => {
2389                    // `repeat: N every: M`
2390                    let kw: Ident = input.parse()?;
2391                    if kw != "every" {
2392                        return Err(syn::Error::new(
2393                            kw.span(),
2394                            "expected `every` after `repeat: N`",
2395                        ));
2396                    }
2397                    input.parse::<Token![:]>()?;
2398                    let period: LitInt = input.parse()?;
2399                    Ok(Schedule::Repeat {
2400                        times: first,
2401                        period,
2402                    })
2403                }
2404                "until" => {
2405                    // `until: deadline every: period`
2406                    let kw: Ident = input.parse()?;
2407                    if kw != "every" {
2408                        return Err(syn::Error::new(
2409                            kw.span(),
2410                            "expected `every` after `until: D`",
2411                        ));
2412                    }
2413                    input.parse::<Token![:]>()?;
2414                    let period: LitInt = input.parse()?;
2415                    Ok(Schedule::Until {
2416                        deadline: first,
2417                        period,
2418                    })
2419                }
2420                other => Err(syn::Error::new(
2421                    kind.span(),
2422                    format!(
2423                        "unknown schedule keyword `{other}`; expected after / every / repeat / until"
2424                    ),
2425                )),
2426            }
2427        }
2428    }
2429
2430    impl Parse for TimerInput {
2431        fn parse(input: ParseStream) -> syn::Result<Self> {
2432            let name: Ident = input.parse()?;
2433            input.parse::<Token![,]>()?;
2434            let schedule: Schedule = input.parse()?;
2435            input.parse::<Token![,]>()?;
2436            let closure: ExprClosure = input.parse()?;
2437            Ok(Self {
2438                name,
2439                schedule,
2440                closure,
2441            })
2442        }
2443    }
2444
2445    pub fn expand(input: TokenStream) -> TokenStream {
2446        let parsed = match parse2::<TimerInput>(input) {
2447            Ok(v) => v,
2448            Err(e) => return e.to_compile_error(),
2449        };
2450
2451        let name = &parsed.name;
2452        let closure = &parsed.closure;
2453
2454        let ctor = match &parsed.schedule {
2455            Schedule::After(p) => quote! { mirui::core::timer::Timer::after(#p, __cb) },
2456            Schedule::Every(p) => quote! { mirui::core::timer::Timer::every(#p, __cb) },
2457            Schedule::Repeat { times, period } => {
2458                quote! { mirui::core::timer::Timer::repeat(#times, #period, __cb) }
2459            }
2460            Schedule::Until { deadline, period } => {
2461                quote! { mirui::core::timer::Timer::until(#deadline, #period, __cb) }
2462            }
2463        };
2464
2465        quote! {
2466            pub struct #name;
2467            impl #name {
2468                pub fn install(world: &mut mirui::ecs::World) -> mirui::ecs::Entity {
2469                    let __cb: fn(&mut mirui::ecs::World, mirui::ecs::Entity) = #closure;
2470                    let e = world.spawn_empty();
2471                    world.insert(e, #ctor);
2472                    e
2473                }
2474            }
2475        }
2476    }
2477}
2478
2479/// Define a motion component (Tween or Spring) + its tick/apply system.
2480///
2481/// ```rust,ignore
2482/// animate!(AnimateX, |world, entity, value| {
2483///     mirui::ui::set_position(world, entity, value, Fixed::from_int(2));
2484/// });
2485///
2486/// // Generated:
2487/// // - struct AnimateX(pub mirui::anim::Motion)
2488/// // - impl MotionComponent for AnimateX
2489/// // - AnimateX::system() -> fn(&mut World)
2490/// //
2491/// // Usage:
2492/// //   app.add_system(AnimateX::system());
2493/// //   world.insert(e, AnimateX(Tween::ease_to(from, to, 250).into()));
2494/// //   world.insert(e, AnimateX(Spring::preset(from, to, SMOOTH).into()));
2495/// ```
2496#[proc_macro]
2497pub fn animate(input: TokenStream) -> TokenStream {
2498    animate_impl::expand(input.into()).into()
2499}
2500
2501mod animate_impl {
2502    use proc_macro2::TokenStream;
2503    use quote::quote;
2504    use syn::parse::{Parse, ParseStream};
2505    use syn::{ExprClosure, Ident, Token, parse2};
2506
2507    struct AnimateInput {
2508        name: Ident,
2509        closure: ExprClosure,
2510    }
2511
2512    impl Parse for AnimateInput {
2513        fn parse(input: ParseStream) -> syn::Result<Self> {
2514            let name: Ident = input.parse()?;
2515            input.parse::<Token![,]>()?;
2516            let closure: ExprClosure = input.parse()?;
2517            Ok(Self { name, closure })
2518        }
2519    }
2520
2521    pub fn expand(input: TokenStream) -> TokenStream {
2522        let parsed = match parse2::<AnimateInput>(input) {
2523            Ok(v) => v,
2524            Err(e) => return e.to_compile_error(),
2525        };
2526
2527        let name = &parsed.name;
2528        let closure = &parsed.closure;
2529
2530        quote! {
2531            pub struct #name(pub mirui::anim::Motion);
2532
2533            impl mirui::anim::MotionComponent for #name {
2534                fn motion(&self) -> &mirui::anim::Motion { &self.0 }
2535                fn motion_mut(&mut self) -> &mut mirui::anim::Motion { &mut self.0 }
2536            }
2537
2538            impl #name {
2539                pub fn system() -> mirui::ecs::System {
2540                    fn __sys(world: &mut mirui::ecs::World) {
2541                        mirui::anim::run_motion::<#name>(world, #closure);
2542                    }
2543                    mirui::ecs::System::new(
2544                        stringify!(#name),
2545                        mirui::ecs::run_order::ANIMATION,
2546                        __sys,
2547                    )
2548                }
2549            }
2550        }
2551    }
2552}
2553
2554/// Mints unique guard idents for `trace_span!` so multiple calls in
2555/// the same scope don't shadow each other. Overflow at 2³² is
2556/// theoretical only.
2557static TRACE_SPAN_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
2558
2559fn next_trace_span_id() -> u32 {
2560    TRACE_SPAN_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2561}
2562
2563mod trace_span_input {
2564    use syn::parse::{Parse, ParseStream};
2565    use syn::{Block, LitStr, Token};
2566
2567    pub enum TraceSpanInput {
2568        Statement(LitStr),
2569        Expression(LitStr, Block),
2570    }
2571
2572    impl Parse for TraceSpanInput {
2573        fn parse(input: ParseStream) -> syn::Result<Self> {
2574            let name: LitStr = input.parse()?;
2575            if input.is_empty() {
2576                Ok(TraceSpanInput::Statement(name))
2577            } else {
2578                input.parse::<Token![,]>()?;
2579                let body: Block = input.parse()?;
2580                Ok(TraceSpanInput::Expression(name, body))
2581            }
2582        }
2583    }
2584}
2585
2586/// `trace_span!("name")` — RAII statement: guard lives until end of
2587/// scope. Multiple calls in one scope each get a unique binding.
2588///
2589/// `trace_span!("name", { ... })` — block expression form,
2590/// evaluates to the block's value.
2591#[proc_macro]
2592pub fn trace_span(input: TokenStream) -> TokenStream {
2593    let parsed = parse_macro_input!(input as trace_span_input::TraceSpanInput);
2594    match parsed {
2595        trace_span_input::TraceSpanInput::Statement(name) => {
2596            let id = next_trace_span_id();
2597            let ident = quote::format_ident!("__trace_span_guard_{}", id);
2598            quote::quote! {
2599                let #ident = mirui::core::perf::enter(#name);
2600            }
2601            .into()
2602        }
2603        trace_span_input::TraceSpanInput::Expression(name, body) => {
2604            let id = next_trace_span_id();
2605            let ident = quote::format_ident!("__trace_span_guard_{}", id);
2606            quote::quote! {{
2607                let #ident = mirui::core::perf::enter(#name);
2608                let __trace_span_value = #body;
2609                drop(#ident);
2610                __trace_span_value
2611            }}
2612            .into()
2613        }
2614    }
2615}
2616
2617/// `#[trace_fn("name")]` — equivalent to `trace_span!("name");` as
2618/// the first statement of the fn body.
2619#[proc_macro_attribute]
2620pub fn trace_fn(args: TokenStream, item: TokenStream) -> TokenStream {
2621    let name = parse_macro_input!(args as syn::LitStr);
2622    let mut func = parse_macro_input!(item as syn::ItemFn);
2623    let stmts = &func.block.stmts;
2624    let id = next_trace_span_id();
2625    let ident = quote::format_ident!("__trace_span_guard_{}", id);
2626    *func.block = syn::parse_quote! {{
2627        let #ident = mirui::core::perf::enter(#name);
2628        #(#stmts)*
2629    }};
2630    quote::quote! { #func }.into()
2631}
2632
2633mod system_attr {
2634    use syn::parse::{Parse, ParseStream};
2635    use syn::{Expr, Ident, LitStr, Token, Type, bracketed, punctuated::Punctuated};
2636
2637    pub struct SystemArgs {
2638        pub name: Option<LitStr>,
2639        pub order: Option<Expr>,
2640        /// Component type(s) gating this system. Empty = always runs.
2641        /// Multiple entries are OR-combined (any present triggers run).
2642        pub expect: Vec<Type>,
2643    }
2644
2645    impl Parse for SystemArgs {
2646        fn parse(input: ParseStream) -> syn::Result<Self> {
2647            let mut name: Option<LitStr> = None;
2648            let mut order: Option<Expr> = None;
2649            let mut expect: Vec<Type> = Vec::new();
2650            while !input.is_empty() {
2651                let key: Ident = input.parse()?;
2652                input.parse::<Token![=]>()?;
2653                match key.to_string().as_str() {
2654                    "name" => name = Some(input.parse()?),
2655                    "order" => order = Some(input.parse()?),
2656                    "expect" => {
2657                        if input.peek(syn::token::Bracket) {
2658                            let content;
2659                            bracketed!(content in input);
2660                            let types: Punctuated<Type, Token![,]> =
2661                                content.parse_terminated(Type::parse, Token![,])?;
2662                            expect.extend(types);
2663                        } else {
2664                            expect.push(input.parse()?);
2665                        }
2666                    }
2667                    other => {
2668                        return Err(syn::Error::new(
2669                            key.span(),
2670                            format!(
2671                                "unknown #[system] arg `{other}`; expected `name`, `order`, or `expect`",
2672                            ),
2673                        ));
2674                    }
2675                }
2676                if input.is_empty() {
2677                    break;
2678                }
2679                input.parse::<Token![,]>()?;
2680            }
2681            Ok(Self {
2682                name,
2683                order,
2684                expect,
2685            })
2686        }
2687    }
2688}
2689
2690/// Attach perf-aware metadata to a `fn(&mut World)`.
2691///
2692/// Generates a sibling module sharing the fn's ident exposing
2693/// `system()` returning a [`mirui::ecs::System`] with the configured
2694/// name + run_order slot. The fn itself is left intact so direct
2695/// calls (tests, manual invocation) still work.
2696///
2697/// ```ignore
2698/// #[mirui::system(order = ANIMATION)]
2699/// fn spin_system(world: &mut World) { /* ... */ }
2700///
2701/// spin_system(world);                     // direct call
2702/// app.add_system(spin_system::system());  // scheduled
2703/// ```
2704///
2705/// Defaults: `name` derives from the fn ident; `order` defaults to
2706/// `run_order::NORMAL`. `order` accepts either a `run_order::*`
2707/// constant or a literal `i32`.
2708#[proc_macro_attribute]
2709pub fn system(args: TokenStream, item: TokenStream) -> TokenStream {
2710    let args = parse_macro_input!(args as system_attr::SystemArgs);
2711    let func = parse_macro_input!(item as syn::ItemFn);
2712    let fn_ident = &func.sig.ident;
2713    let fn_vis = &func.vis;
2714    let name_lit = args
2715        .name
2716        .unwrap_or_else(|| syn::LitStr::new(&fn_ident.to_string(), fn_ident.span()));
2717    let order_expr: syn::Expr = match args.order {
2718        Some(e) => e,
2719        None => syn::parse_quote!(mirui::ecs::run_order::NORMAL),
2720    };
2721    let expect_const_ident =
2722        quote::format_ident!("__MIRUI_EXPECT_{}", fn_ident.to_string().to_uppercase());
2723    let expect_outer = if args.expect.is_empty() {
2724        quote::quote! {}
2725    } else {
2726        let entries = args.expect.iter().map(|ty| {
2727            quote::quote! { (::core::any::TypeId::of::<#ty>) as fn() -> ::core::any::TypeId }
2728        });
2729        quote::quote! {
2730            #[doc(hidden)]
2731            #[allow(non_upper_case_globals)]
2732            const #expect_const_ident: &[fn() -> ::core::any::TypeId] = &[ #(#entries),* ];
2733        }
2734    };
2735    let with_expect_call = if args.expect.is_empty() {
2736        quote::quote! {}
2737    } else {
2738        quote::quote! { .with_expect(super::#expect_const_ident) }
2739    };
2740    quote::quote! {
2741        #func
2742        #expect_outer
2743
2744        #[allow(non_snake_case, non_camel_case_types)]
2745        #fn_vis mod #fn_ident {
2746            #[allow(unused_imports)]
2747            use mirui::ecs::run_order::*;
2748            pub const fn system() -> mirui::ecs::System {
2749                mirui::ecs::System::new(#name_lit, #order_expr, super::#fn_ident) #with_expect_call
2750            }
2751        }
2752    }
2753    .into()
2754}
2755
2756#[proc_macro_derive(Component)]
2757pub fn derive_component(input: TokenStream) -> TokenStream {
2758    let input = parse_macro_input!(input as syn::DeriveInput);
2759    let name = &input.ident;
2760    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
2761    quote! {
2762        impl #impl_generics ::mirui::ecs::Component for #name #ty_generics #where_clause {}
2763    }
2764    .into()
2765}
2766
2767#[cfg(test)]
2768mod widget_kind_tests {
2769    use super::*;
2770    use proc_macro2::Span;
2771
2772    fn id(s: &str) -> syn::Ident {
2773        syn::Ident::new(s, Span::call_site())
2774    }
2775
2776    #[test]
2777    fn lowercase_is_illegal() {
2778        assert_eq!(
2779            classify_widget_name(&id("button")),
2780            WidgetKind::IllegalLowercase
2781        );
2782        assert_eq!(
2783            classify_widget_name(&id("dark_btn")),
2784            WidgetKind::IllegalLowercase
2785        );
2786    }
2787
2788    #[test]
2789    fn reserved_names_are_layout() {
2790        assert_eq!(classify_widget_name(&id("View")), WidgetKind::Layout);
2791        assert_eq!(classify_widget_name(&id("Row")), WidgetKind::Layout);
2792        assert_eq!(classify_widget_name(&id("Column")), WidgetKind::Layout);
2793        assert_eq!(classify_widget_name(&id("Scroll")), WidgetKind::Layout);
2794    }
2795
2796    #[test]
2797    fn other_capital_names_are_component() {
2798        assert_eq!(classify_widget_name(&id("Button")), WidgetKind::Component);
2799        assert_eq!(classify_widget_name(&id("MyCard")), WidgetKind::Component);
2800        assert_eq!(classify_widget_name(&id("Checkbox")), WidgetKind::Component);
2801    }
2802}