1extern crate proc_macro;
2
3mod compose;
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::parse_macro_input;
8
9use xrune::ds_node::ds_attr::DsAttr;
10use xrune::ds_node::{DsRoot, DsTreeRef};
11use xrune::ds_rune::DsRune;
12use xrune::ds_rune::decipher::decipher;
13
14enum Cmd {
15 Widget(WidgetCmd),
16 Iter(IterCmd),
17 If(IfCmd),
18}
19
20struct WidgetCmd {
21 var: syn::Ident,
22 attrs: Vec<proc_macro2::TokenStream>,
23 layout_fields: Vec<proc_macro2::TokenStream>,
24 errors: Vec<proc_macro2::TokenStream>,
25 enchants: Vec<proc_macro2::TokenStream>,
26 children: Vec<Cmd>,
27}
28
29struct IterCmd {
30 iterable: proc_macro2::TokenStream,
31 variable: syn::Ident,
32 body: Vec<Cmd>,
33}
34
35struct IfCmd {
36 condition: proc_macro2::TokenStream,
37 body: Vec<Cmd>,
38}
39
40struct MiruiRune {
41 world_expr: proc_macro2::TokenStream,
42 parent_expr: proc_macro2::TokenStream,
43 stack: Vec<Vec<Cmd>>,
44 counter: usize,
45}
46
47impl MiruiRune {
48 fn new() -> Self {
49 Self {
50 world_expr: quote! { __world },
51 parent_expr: quote! { __parent },
52 stack: vec![Vec::new()],
53 counter: 0,
54 }
55 }
56
57 fn next_var(&mut self) -> syn::Ident {
58 let name = format!("__w{}", self.counter);
59 self.counter += 1;
60 syn::Ident::new(&name, proc_macro2::Span::call_site())
61 }
62
63 fn parse_attrs(
64 attrs: &[DsAttr],
65 ) -> (
66 Vec<proc_macro2::TokenStream>,
67 Vec<proc_macro2::TokenStream>,
68 Vec<proc_macro2::TokenStream>,
69 Vec<proc_macro2::TokenStream>,
70 ) {
71 let mut builder_calls = Vec::new();
72 let mut layout_fields = Vec::new();
73 let mut errors = Vec::new();
74 let component_inserts = Vec::new();
75
76 for attr in attrs {
77 let name = attr.name.to_string();
78 let value = &attr.value;
79 match name.as_str() {
80 "bg_color" => builder_calls.push(quote! { .bg_color(#value) }),
81 "text" => builder_calls.push(quote! { .text(#value) }),
82 "text_color" => builder_calls.push(quote! { .text_color(#value) }),
83 "border_radius" => builder_calls.push(quote! { .border_radius(#value) }),
84 "clip_children" => builder_calls.push(quote! { .clip_children(#value) }),
85 "border_color" => builder_calls.push(quote! { .border(#value, 1) }),
86 "border_width" => builder_calls.push(quote! { .border_width(#value) }),
87 "width" => layout_fields.push(quote! { width: mirui::types::Dimension::Px(mirui::types::Fixed::from_int(#value as i32)) }),
88 "height" => layout_fields.push(quote! { height: mirui::types::Dimension::Px(mirui::types::Fixed::from_int(#value as i32)) }),
89 "grow" => layout_fields.push(quote! { grow: mirui::types::Fixed::from_f32(#value) }),
90 "direction" => layout_fields.push(quote! { direction: #value }),
91 "justify" => layout_fields.push(quote! { justify: #value }),
92 "align" => layout_fields.push(quote! { align: #value }),
93 "padding" => layout_fields.push(quote! { padding: #value }),
94 "position" => layout_fields.push(quote! { position: #value }),
95 "left" => layout_fields.push(quote! { left: mirui::types::Dimension::Px(mirui::types::Fixed::from_int(#value)) }),
96 "top" => layout_fields.push(quote! { top: mirui::types::Dimension::Px(mirui::types::Fixed::from_int(#value)) }),
97 "image" => builder_calls.push(quote! { .image(#value) }),
98 unknown => {
99 let msg = format!("unknown widget attribute `{unknown}`");
100 errors.push(syn::Error::new(attr.name.span(), msg).to_compile_error());
101 }
102 }
103 }
104 (builder_calls, layout_fields, errors, component_inserts)
105 }
106
107 fn emit_cmd(
110 cmd: &Cmd,
111 world: &proc_macro2::TokenStream,
112 parent_var: &proc_macro2::TokenStream,
113 ) -> proc_macro2::TokenStream {
114 match cmd {
115 Cmd::Widget(w) => Self::emit_widget(w, world),
116 Cmd::Iter(i) => Self::emit_iter(i, world, parent_var),
117 Cmd::If(i) => Self::emit_if(i, world, parent_var),
118 }
119 }
120
121 fn emit_widget(cmd: &WidgetCmd, world: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
122 let var = &cmd.var;
123 let attrs = &cmd.attrs;
124 let layout_fields = &cmd.layout_fields;
125 let errors = &cmd.errors;
126
127 let mut tokens = proc_macro2::TokenStream::new();
128
129 for e in errors {
130 tokens.extend(e.clone());
131 }
132
133 let var_ts = quote! { #var };
135 let mut child_vars = Vec::new();
136 let mut deferred_iters = Vec::new();
137
138 for child in &cmd.children {
139 match child {
140 Cmd::Widget(w) => {
141 tokens.extend(Self::emit_widget(w, world));
142 child_vars.push(&w.var);
143 }
144 Cmd::Iter(_) | Cmd::If(_) => {
145 deferred_iters.push(child);
146 }
147 }
148 }
149
150 let layout_call = if layout_fields.is_empty() {
152 quote! {}
153 } else {
154 quote! { .layout(mirui::layout::LayoutStyle { #(#layout_fields,)* ..Default::default() }) }
155 };
156
157 let child_calls: Vec<proc_macro2::TokenStream> =
158 child_vars.iter().map(|c| quote! { .child(#c) }).collect();
159
160 tokens.extend(quote! {
161 let #var = mirui::widget::builder::WidgetBuilder::new(#world)
162 #(#attrs)*
163 #layout_call
164 #(#child_calls)*
165 .id();
166 });
167
168 let enchants = &cmd.enchants;
170 for enchant in enchants {
171 tokens.extend(quote! {
172 (#world).insert(#var, #enchant);
173 });
174 }
175
176 for iter_cmd in deferred_iters {
178 tokens.extend(Self::emit_cmd(iter_cmd, world, &var_ts));
179 }
180
181 tokens
182 }
183
184 fn emit_iter(
185 cmd: &IterCmd,
186 world: &proc_macro2::TokenStream,
187 parent_var: &proc_macro2::TokenStream,
188 ) -> proc_macro2::TokenStream {
189 let iterable = &cmd.iterable;
190 let variable = &cmd.variable;
191
192 let mut body_tokens = proc_macro2::TokenStream::new();
194 for child in &cmd.body {
195 body_tokens.extend(Self::emit_cmd(child, world, parent_var));
196 if let Cmd::Widget(w) = child {
198 let child_var = &w.var;
199 body_tokens.extend(quote! {
200 {
201 use mirui::widget::{Children, Parent};
202 (#world).insert(#child_var, Parent(#parent_var));
203 if let Some(children) = (#world).get_mut::<Children>(#parent_var) {
204 children.0.push(#child_var);
205 }
206 }
207 });
208 }
209 }
210
211 quote! {
212 for #variable in #iterable {
213 #body_tokens
214 }
215 }
216 }
217
218 fn emit_if(
219 cmd: &IfCmd,
220 world: &proc_macro2::TokenStream,
221 parent_var: &proc_macro2::TokenStream,
222 ) -> proc_macro2::TokenStream {
223 let condition = &cmd.condition;
224
225 let mut body_tokens = proc_macro2::TokenStream::new();
226 for child in &cmd.body {
227 body_tokens.extend(Self::emit_cmd(child, world, parent_var));
228 if let Cmd::Widget(w) = child {
229 let child_var = &w.var;
230 body_tokens.extend(quote! {
231 {
232 use mirui::widget::{Children, Parent};
233 (#world).insert(#child_var, Parent(#parent_var));
234 if let Some(children) = (#world).get_mut::<Children>(#parent_var) {
235 children.0.push(#child_var);
236 }
237 }
238 });
239 }
240 }
241
242 quote! {
243 if #condition {
244 #body_tokens
245 }
246 }
247 }
248}
249
250impl DsRune for MiruiRune {
251 fn inscribe_root(&mut self, _parent_expr: &syn::Expr) {}
252
253 fn inscribe_widget(
254 &mut self,
255 _name: &syn::Ident,
256 attrs: &[DsAttr],
257 enchants: &[syn::Expr],
258 children: &[DsTreeRef],
259 ) {
260 let var = self.next_var();
261 let (builder_calls, layout_fields, errors, component_inserts) = Self::parse_attrs(attrs);
262 let mut enchant_tokens: Vec<proc_macro2::TokenStream> = component_inserts;
263 enchant_tokens.extend(enchants.iter().map(|e| quote! { #e }));
264
265 self.stack.push(Vec::new());
266 for child in children {
267 decipher(child, self);
268 }
269 let my_children = self.stack.pop().unwrap();
270
271 let cmd = Cmd::Widget(WidgetCmd {
272 var,
273 attrs: builder_calls,
274 layout_fields,
275 errors,
276 enchants: enchant_tokens,
277 children: my_children,
278 });
279
280 self.stack.last_mut().unwrap().push(cmd);
281 }
282
283 fn inscribe_if(&mut self, condition: &syn::Expr, children: &[DsTreeRef]) {
284 self.stack.push(Vec::new());
285 for child in children {
286 decipher(child, self);
287 }
288 let body = self.stack.pop().unwrap();
289
290 let cmd = Cmd::If(IfCmd {
291 condition: quote! { #condition },
292 body,
293 });
294
295 self.stack.last_mut().unwrap().push(cmd);
296 }
297
298 fn inscribe_iter(
299 &mut self,
300 iterable: &syn::Expr,
301 variable: &syn::Ident,
302 children: &[DsTreeRef],
303 ) {
304 self.stack.push(Vec::new());
305 for child in children {
306 decipher(child, self);
307 }
308 let body = self.stack.pop().unwrap();
309
310 let cmd = Cmd::Iter(IterCmd {
311 iterable: quote! { #iterable },
312 variable: variable.clone(),
313 body,
314 });
315
316 self.stack.last_mut().unwrap().push(cmd);
317 }
318
319 fn seal(self) -> proc_macro2::TokenStream {
320 let world = &self.world_expr;
321 let parent_entity = &self.parent_expr;
322 let mut tokens = proc_macro2::TokenStream::new();
323
324 let root_cmds = &self.stack[0];
325 for cmd in root_cmds {
326 tokens.extend(Self::emit_cmd(cmd, world, parent_entity));
327 }
328
329 let mut last_var = None;
331 for cmd in root_cmds {
332 if let Cmd::Widget(w) = cmd {
333 let var = &w.var;
334 last_var = Some(var.clone());
335 tokens.extend(quote! {
336 {
337 use mirui::widget::{Children, Parent};
338 (#world).insert(#var, Parent(#parent_entity));
339 if let Some(children) = (#world).get_mut::<Children>(#parent_entity) {
340 children.0.push(#var);
341 }
342 }
343 });
344 }
345 }
346
347 if let Some(var) = last_var {
349 quote! { { #tokens #var } }
350 } else {
351 quote! { { #tokens } }
352 }
353 }
354}
355
356#[proc_macro]
357pub fn ui(input: TokenStream) -> TokenStream {
358 let root = parse_macro_input!(input as DsRoot);
359 let mut rune = MiruiRune::new();
360
361 let context_attrs = root.get_context_attrs();
362 if let Some(world_attr) = context_attrs.iter().find(|a| a.name == "world") {
363 let world_expr = &world_attr.value;
364 rune.world_expr = quote! { #world_expr };
365 } else {
366 return syn::Error::new(proc_macro2::Span::call_site(), "missing `world` in context")
367 .to_compile_error()
368 .into();
369 }
370
371 let parent = root.get_parent();
372 rune.parent_expr = quote! { #parent };
373
374 rune.inscribe_root(&root.get_parent());
375 let content = root.get_content();
376 decipher(&content, &mut rune);
377 TokenStream::from(rune.seal())
378}
379
380#[proc_macro]
381pub fn compose_backend(input: TokenStream) -> TokenStream {
382 compose::expand(input.into()).into()
383}
384
385#[proc_macro]
393pub fn timer(input: TokenStream) -> TokenStream {
394 timer_impl::expand(input.into()).into()
395}
396
397mod timer_impl {
398 use proc_macro2::TokenStream;
399 use quote::quote;
400 use syn::parse::{Parse, ParseStream};
401 use syn::{ExprClosure, Ident, LitInt, Token, parse2};
402
403 enum Schedule {
404 After(LitInt),
405 Every(LitInt),
406 Repeat { times: LitInt, period: LitInt },
407 Until { deadline: LitInt, period: LitInt },
408 }
409
410 struct TimerInput {
411 name: Ident,
412 schedule: Schedule,
413 closure: ExprClosure,
414 }
415
416 impl Parse for Schedule {
417 fn parse(input: ParseStream) -> syn::Result<Self> {
418 let kind: Ident = input.parse()?;
419 input.parse::<Token![:]>()?;
420 let first: LitInt = input.parse()?;
421 match kind.to_string().as_str() {
422 "after" => Ok(Schedule::After(first)),
423 "every" => Ok(Schedule::Every(first)),
424 "repeat" => {
425 let kw: Ident = input.parse()?;
427 if kw != "every" {
428 return Err(syn::Error::new(
429 kw.span(),
430 "expected `every` after `repeat: N`",
431 ));
432 }
433 input.parse::<Token![:]>()?;
434 let period: LitInt = input.parse()?;
435 Ok(Schedule::Repeat {
436 times: first,
437 period,
438 })
439 }
440 "until" => {
441 let kw: Ident = input.parse()?;
443 if kw != "every" {
444 return Err(syn::Error::new(
445 kw.span(),
446 "expected `every` after `until: D`",
447 ));
448 }
449 input.parse::<Token![:]>()?;
450 let period: LitInt = input.parse()?;
451 Ok(Schedule::Until {
452 deadline: first,
453 period,
454 })
455 }
456 other => Err(syn::Error::new(
457 kind.span(),
458 format!(
459 "unknown schedule keyword `{other}`; expected after / every / repeat / until"
460 ),
461 )),
462 }
463 }
464 }
465
466 impl Parse for TimerInput {
467 fn parse(input: ParseStream) -> syn::Result<Self> {
468 let name: Ident = input.parse()?;
469 input.parse::<Token![,]>()?;
470 let schedule: Schedule = input.parse()?;
471 input.parse::<Token![,]>()?;
472 let closure: ExprClosure = input.parse()?;
473 Ok(Self {
474 name,
475 schedule,
476 closure,
477 })
478 }
479 }
480
481 pub fn expand(input: TokenStream) -> TokenStream {
482 let parsed = match parse2::<TimerInput>(input) {
483 Ok(v) => v,
484 Err(e) => return e.to_compile_error(),
485 };
486
487 let name = &parsed.name;
488 let closure = &parsed.closure;
489
490 let ctor = match &parsed.schedule {
491 Schedule::After(p) => quote! { mirui::timer::Timer::after(#p, __cb) },
492 Schedule::Every(p) => quote! { mirui::timer::Timer::every(#p, __cb) },
493 Schedule::Repeat { times, period } => {
494 quote! { mirui::timer::Timer::repeat(#times, #period, __cb) }
495 }
496 Schedule::Until { deadline, period } => {
497 quote! { mirui::timer::Timer::until(#deadline, #period, __cb) }
498 }
499 };
500
501 quote! {
502 pub struct #name;
503 impl #name {
504 pub fn install(world: &mut mirui::ecs::World) -> mirui::ecs::Entity {
505 let __cb: fn(&mut mirui::ecs::World, mirui::ecs::Entity) = #closure;
506 let e = world.spawn();
507 world.insert(e, #ctor);
508 e
509 }
510 }
511 }
512 }
513}
514
515#[proc_macro]
533pub fn animate(input: TokenStream) -> TokenStream {
534 animate_impl::expand(input.into()).into()
535}
536
537mod animate_impl {
538 use proc_macro2::TokenStream;
539 use quote::quote;
540 use syn::parse::{Parse, ParseStream};
541 use syn::{ExprClosure, Ident, Token, parse2};
542
543 struct AnimateInput {
544 name: Ident,
545 closure: ExprClosure,
546 }
547
548 impl Parse for AnimateInput {
549 fn parse(input: ParseStream) -> syn::Result<Self> {
550 let name: Ident = input.parse()?;
551 input.parse::<Token![,]>()?;
552 let closure: ExprClosure = input.parse()?;
553 Ok(Self { name, closure })
554 }
555 }
556
557 pub fn expand(input: TokenStream) -> TokenStream {
558 let parsed = match parse2::<AnimateInput>(input) {
559 Ok(v) => v,
560 Err(e) => return e.to_compile_error(),
561 };
562
563 let name = &parsed.name;
564 let closure = &parsed.closure;
565
566 quote! {
567 pub struct #name(pub mirui::anim::Motion);
568
569 impl mirui::anim::MotionComponent for #name {
570 fn motion(&self) -> &mirui::anim::Motion { &self.0 }
571 fn motion_mut(&mut self) -> &mut mirui::anim::Motion { &mut self.0 }
572 }
573
574 impl #name {
575 pub fn system() -> fn(&mut mirui::ecs::World) {
576 fn __sys(world: &mut mirui::ecs::World) {
577 mirui::anim::run_motion::<#name>(world, #closure);
578 }
579 __sys
580 }
581 }
582 }
583 }
584}
585
586static TRACE_SPAN_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
590
591fn next_trace_span_id() -> u32 {
592 TRACE_SPAN_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
593}
594
595mod trace_span_input {
596 use syn::parse::{Parse, ParseStream};
597 use syn::{Block, LitStr, Token};
598
599 pub enum TraceSpanInput {
600 Statement(LitStr),
601 Expression(LitStr, Block),
602 }
603
604 impl Parse for TraceSpanInput {
605 fn parse(input: ParseStream) -> syn::Result<Self> {
606 let name: LitStr = input.parse()?;
607 if input.is_empty() {
608 Ok(TraceSpanInput::Statement(name))
609 } else {
610 input.parse::<Token![,]>()?;
611 let body: Block = input.parse()?;
612 Ok(TraceSpanInput::Expression(name, body))
613 }
614 }
615 }
616}
617
618#[proc_macro]
624pub fn trace_span(input: TokenStream) -> TokenStream {
625 let parsed = parse_macro_input!(input as trace_span_input::TraceSpanInput);
626 match parsed {
627 trace_span_input::TraceSpanInput::Statement(name) => {
628 let id = next_trace_span_id();
629 let ident = quote::format_ident!("__trace_span_guard_{}", id);
630 quote::quote! {
631 let #ident = mirui::perf::enter(#name);
632 }
633 .into()
634 }
635 trace_span_input::TraceSpanInput::Expression(name, body) => {
636 let id = next_trace_span_id();
637 let ident = quote::format_ident!("__trace_span_guard_{}", id);
638 quote::quote! {{
639 let #ident = mirui::perf::enter(#name);
640 let __trace_span_value = #body;
641 drop(#ident);
642 __trace_span_value
643 }}
644 .into()
645 }
646 }
647}
648
649#[proc_macro_attribute]
652pub fn trace_fn(args: TokenStream, item: TokenStream) -> TokenStream {
653 let name = parse_macro_input!(args as syn::LitStr);
654 let mut func = parse_macro_input!(item as syn::ItemFn);
655 let stmts = &func.block.stmts;
656 let id = next_trace_span_id();
657 let ident = quote::format_ident!("__trace_span_guard_{}", id);
658 *func.block = syn::parse_quote! {{
659 let #ident = mirui::perf::enter(#name);
660 #(#stmts)*
661 }};
662 quote::quote! { #func }.into()
663}
664
665mod system_attr {
666 use syn::parse::{Parse, ParseStream};
667 use syn::{Expr, Ident, LitStr, Token, Type, bracketed, punctuated::Punctuated};
668
669 pub struct SystemArgs {
670 pub name: Option<LitStr>,
671 pub order: Option<Expr>,
672 pub expect: Vec<Type>,
675 }
676
677 impl Parse for SystemArgs {
678 fn parse(input: ParseStream) -> syn::Result<Self> {
679 let mut name: Option<LitStr> = None;
680 let mut order: Option<Expr> = None;
681 let mut expect: Vec<Type> = Vec::new();
682 while !input.is_empty() {
683 let key: Ident = input.parse()?;
684 input.parse::<Token![=]>()?;
685 match key.to_string().as_str() {
686 "name" => name = Some(input.parse()?),
687 "order" => order = Some(input.parse()?),
688 "expect" => {
689 if input.peek(syn::token::Bracket) {
690 let content;
691 bracketed!(content in input);
692 let types: Punctuated<Type, Token![,]> =
693 content.parse_terminated(Type::parse, Token![,])?;
694 expect.extend(types);
695 } else {
696 expect.push(input.parse()?);
697 }
698 }
699 other => {
700 return Err(syn::Error::new(
701 key.span(),
702 format!(
703 "unknown #[system] arg `{other}`; expected `name`, `order`, or `expect`",
704 ),
705 ));
706 }
707 }
708 if input.is_empty() {
709 break;
710 }
711 input.parse::<Token![,]>()?;
712 }
713 Ok(Self {
714 name,
715 order,
716 expect,
717 })
718 }
719 }
720}
721
722#[proc_macro_attribute]
741pub fn system(args: TokenStream, item: TokenStream) -> TokenStream {
742 let args = parse_macro_input!(args as system_attr::SystemArgs);
743 let func = parse_macro_input!(item as syn::ItemFn);
744 let fn_ident = &func.sig.ident;
745 let fn_vis = &func.vis;
746 let name_lit = args
747 .name
748 .unwrap_or_else(|| syn::LitStr::new(&fn_ident.to_string(), fn_ident.span()));
749 let order_expr: syn::Expr = match args.order {
750 Some(e) => e,
751 None => syn::parse_quote!(mirui::ecs::run_order::NORMAL),
752 };
753 let expect_const_ident =
754 quote::format_ident!("__MIRUI_EXPECT_{}", fn_ident.to_string().to_uppercase());
755 let expect_outer = if args.expect.is_empty() {
756 quote::quote! {}
757 } else {
758 let entries = args.expect.iter().map(|ty| {
759 quote::quote! { (::core::any::TypeId::of::<#ty>) as fn() -> ::core::any::TypeId }
760 });
761 quote::quote! {
762 #[doc(hidden)]
763 #[allow(non_upper_case_globals)]
764 const #expect_const_ident: &[fn() -> ::core::any::TypeId] = &[ #(#entries),* ];
765 }
766 };
767 let with_expect_call = if args.expect.is_empty() {
768 quote::quote! {}
769 } else {
770 quote::quote! { .with_expect(super::#expect_const_ident) }
771 };
772 quote::quote! {
773 #func
774 #expect_outer
775
776 #[allow(non_snake_case, non_camel_case_types)]
777 #fn_vis mod #fn_ident {
778 #[allow(unused_imports)]
779 use mirui::ecs::run_order::*;
780 pub const fn system() -> mirui::ecs::System {
781 mirui::ecs::System::new(#name_lit, #order_expr, super::#fn_ident) #with_expect_call
782 }
783 }
784 }
785 .into()
786}