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
408struct 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 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 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 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 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 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 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 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 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 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 if let Some(var) = last_var {
2144 quote! { { #tokens #var } }
2145 } else {
2146 quote! { { #tokens } }
2147 }
2148 }
2149}
2150
2151#[proc_macro_attribute]
2182pub fn compose(attr: TokenStream, item: TokenStream) -> TokenStream {
2183 compose_attr::expand(attr.into(), item.into()).into()
2184}
2185
2186#[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#[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 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 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#[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
2554static 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#[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#[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 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#[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}