Skip to main content

telex_macro/
lib.rs

1//! Procedural macros for Telex.
2//!
3//! - `state!` — creates order-independent state (no hook ordering rules)
4//! - `effect!` — creates order-independent effects with dependencies
5//! - `effect_once!` — creates order-independent effects that run once
6//! - `with!` — clones state handles into closures
7//! - `view!` — JSX-like syntax for building UI trees
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::{format_ident, quote};
12use std::sync::atomic::{AtomicU64, Ordering};
13use syn::{
14    braced,
15    parse::{Parse, ParseStream},
16    parse_macro_input,
17    punctuated::Punctuated,
18    Expr, Ident, LitStr, Result, Token,
19};
20
21/// The view! macro for building UI trees with JSX-like syntax.
22#[proc_macro]
23pub fn view(input: TokenStream) -> TokenStream {
24    let node = parse_macro_input!(input as ViewNode);
25    let expanded = node.to_tokens();
26    TokenStream::from(expanded)
27}
28
29/// Input for the with! macro: `ident1, ident2 => expr`
30struct WithInput {
31    idents: Vec<Ident>,
32    expr: Expr,
33}
34
35impl Parse for WithInput {
36    fn parse(input: ParseStream) -> Result<Self> {
37        // Parse comma-separated identifiers
38        let idents: Punctuated<Ident, Token![,]> = Punctuated::parse_separated_nonempty(input)?;
39        let idents: Vec<Ident> = idents.into_iter().collect();
40
41        // Parse the => separator
42        input.parse::<Token![=>]>()?;
43
44        // Parse the expression (typically a closure)
45        let expr: Expr = input.parse()?;
46
47        Ok(WithInput { idents, expr })
48    }
49}
50
51impl WithInput {
52    fn to_tokens(&self) -> TokenStream2 {
53        let clone_statements: Vec<TokenStream2> = self
54            .idents
55            .iter()
56            .map(|ident| quote! { let #ident = #ident.clone(); })
57            .collect();
58
59        let expr = &self.expr;
60
61        quote! {
62            {
63                #(#clone_statements)*
64                #expr
65            }
66        }
67    }
68}
69
70/// Global counter for generating unique type names.
71static STATE_COUNTER: AtomicU64 = AtomicU64::new(0);
72
73/// Input for the state! macro: `cx, || init_expr`
74struct StateInput {
75    scope: Expr,
76    init: Expr,
77}
78
79impl Parse for StateInput {
80    fn parse(input: ParseStream) -> Result<Self> {
81        // Parse the scope expression (usually just `cx`)
82        let scope: Expr = input.parse()?;
83
84        // Parse the comma separator
85        input.parse::<Token![,]>()?;
86
87        // Parse the initializer expression (usually a closure)
88        let init: Expr = input.parse()?;
89
90        Ok(StateInput { scope, init })
91    }
92}
93
94impl StateInput {
95    fn to_tokens(&self) -> TokenStream2 {
96        let scope = &self.scope;
97        let init = &self.init;
98
99        // Generate a unique type name using an atomic counter.
100        // This ensures each macro invocation gets a distinct type.
101        let counter = STATE_COUNTER.fetch_add(1, Ordering::SeqCst);
102        let key_type = format_ident!("__State_{}", counter);
103
104        quote! {
105            {
106                struct #key_type;
107                #scope.use_state_keyed::<#key_type, _>(#init)
108            }
109        }
110    }
111}
112
113/// The state! macro for creating order-independent state.
114///
115/// This is the recommended way to create state in Telex. Unlike traditional
116/// hooks, state created with this macro can be used conditionally or in any
117/// order without causing panics.
118///
119/// Each macro invocation creates a unique anonymous type as the key,
120/// ensuring each call site gets its own independent state.
121///
122/// # Examples
123///
124/// Basic usage:
125/// ```ignore
126/// let count = state!(cx, || 0);
127/// ```
128///
129/// Safe in conditionals:
130/// ```ignore
131/// if show_counter {
132///     let count = state!(cx, || 0);  // This is safe!
133/// }
134/// ```
135///
136/// Multiple independent states:
137/// ```ignore
138/// let name = state!(cx, || String::new());
139/// let count = state!(cx, || 0);
140/// let visible = state!(cx, || true);
141/// ```
142#[proc_macro]
143pub fn state(input: TokenStream) -> TokenStream {
144    let input = parse_macro_input!(input as StateInput);
145    let expanded = input.to_tokens();
146    TokenStream::from(expanded)
147}
148
149/// Global counter for generating unique effect type names.
150static EFFECT_COUNTER: AtomicU64 = AtomicU64::new(0);
151
152/// Input for the effect! macro: `cx, deps, |&d| effect_body`
153struct EffectInput {
154    scope: Expr,
155    deps: Expr,
156    effect_fn: Expr,
157}
158
159impl Parse for EffectInput {
160    fn parse(input: ParseStream) -> Result<Self> {
161        // Parse the scope expression (usually just `cx`)
162        let scope: Expr = input.parse()?;
163        input.parse::<Token![,]>()?;
164
165        // Parse the dependencies expression
166        let deps: Expr = input.parse()?;
167        input.parse::<Token![,]>()?;
168
169        // Parse the effect closure
170        let effect_fn: Expr = input.parse()?;
171
172        Ok(EffectInput {
173            scope,
174            deps,
175            effect_fn,
176        })
177    }
178}
179
180impl EffectInput {
181    fn to_tokens(&self) -> TokenStream2 {
182        let scope = &self.scope;
183        let deps = &self.deps;
184        let effect_fn = &self.effect_fn;
185
186        let counter = EFFECT_COUNTER.fetch_add(1, Ordering::SeqCst);
187        let key_type = format_ident!("__Effect_{}", counter);
188
189        quote! {
190            {
191                struct #key_type;
192                #scope.use_effect_keyed::<#key_type, _, _, _>(#deps, #effect_fn)
193            }
194        }
195    }
196}
197
198/// The effect! macro for creating order-independent effects with dependencies.
199///
200/// This is the recommended way to create effects in Telex. Unlike traditional
201/// hooks, effects created with this macro can be used conditionally or in any
202/// order without causing issues.
203///
204/// Each macro invocation creates a unique anonymous type as the key,
205/// ensuring each call site gets its own independent effect.
206///
207/// # Examples
208///
209/// Basic usage - runs when count changes:
210/// ```ignore
211/// effect!(cx, count.get(), |&c| {
212///     println!("count changed to {}", c);
213///     || {}  // cleanup function
214/// });
215/// ```
216///
217/// Safe in conditionals:
218/// ```ignore
219/// if show_logger {
220///     effect!(cx, value.get(), |&v| {
221///         println!("value: {}", v);
222///         || {}
223///     });
224/// }
225/// ```
226///
227/// Multiple dependencies via tuple:
228/// ```ignore
229/// effect!(cx, (a.get(), b.get()), |&(a, b)| {
230///     println!("a={}, b={}", a, b);
231///     || {}
232/// });
233/// ```
234#[proc_macro]
235pub fn effect(input: TokenStream) -> TokenStream {
236    let input = parse_macro_input!(input as EffectInput);
237    let expanded = input.to_tokens();
238    TokenStream::from(expanded)
239}
240
241/// Input for the effect_once! macro: `cx, || effect_body`
242struct EffectOnceInput {
243    scope: Expr,
244    effect_fn: Expr,
245}
246
247impl Parse for EffectOnceInput {
248    fn parse(input: ParseStream) -> Result<Self> {
249        // Parse the scope expression (usually just `cx`)
250        let scope: Expr = input.parse()?;
251        input.parse::<Token![,]>()?;
252
253        // Parse the effect closure
254        let effect_fn: Expr = input.parse()?;
255
256        Ok(EffectOnceInput { scope, effect_fn })
257    }
258}
259
260impl EffectOnceInput {
261    fn to_tokens(&self) -> TokenStream2 {
262        let scope = &self.scope;
263        let effect_fn = &self.effect_fn;
264
265        let counter = EFFECT_COUNTER.fetch_add(1, Ordering::SeqCst);
266        let key_type = format_ident!("__Effect_{}", counter);
267
268        quote! {
269            {
270                struct #key_type;
271                #scope.use_effect_once_keyed::<#key_type, _, _>(#effect_fn)
272            }
273        }
274    }
275}
276
277/// The effect_once! macro for creating order-independent effects that run once.
278///
279/// This is the recommended way to run one-time initialization effects in Telex.
280/// Unlike traditional hooks, effects created with this macro can be used
281/// conditionally or in any order.
282///
283/// Each macro invocation creates a unique anonymous type as the key,
284/// ensuring each call site gets its own independent effect.
285///
286/// # Examples
287///
288/// Basic usage - runs once on first render:
289/// ```ignore
290/// effect_once!(cx, || {
291///     println!("App initialized");
292///     || {
293///         println!("App cleanup");
294///     }
295/// });
296/// ```
297///
298/// Safe in conditionals:
299/// ```ignore
300/// if feature_enabled {
301///     effect_once!(cx, || {
302///         setup_feature();
303///         || cleanup_feature()
304///     });
305/// }
306/// ```
307#[proc_macro]
308pub fn effect_once(input: TokenStream) -> TokenStream {
309    let input = parse_macro_input!(input as EffectOnceInput);
310    let expanded = input.to_tokens();
311    TokenStream::from(expanded)
312}
313
314/// The with! macro for cloning state handles into closures.
315///
316/// State<T> is a handle (like a smart pointer), not the data itself.
317/// When you need to use state in a closure, you must clone the handle
318/// so the closure owns its own copy. This macro makes that pattern concise.
319///
320/// # Examples
321///
322/// Single state:
323/// ```ignore
324/// let count = state!(cx, || 0);
325/// let increment = with!(count => move || count.update(|n| *n += 1));
326/// ```
327///
328/// Multiple states:
329/// ```ignore
330/// let count = state!(cx, || 0);
331/// let name = state!(cx, || String::new());
332///
333/// let handler = with!(count, name => move || {
334///     count.update(|n| *n += 1);
335///     name.set("updated".to_string());
336/// });
337/// ```
338///
339/// The above expands to:
340/// ```ignore
341/// let handler = {
342///     let count = count.clone();
343///     let name = name.clone();
344///     move || {
345///         count.update(|n| *n += 1);
346///         name.set("updated".to_string());
347///     }
348/// };
349/// ```
350#[proc_macro]
351pub fn with(input: TokenStream) -> TokenStream {
352    let input = parse_macro_input!(input as WithInput);
353    let expanded = input.to_tokens();
354    TokenStream::from(expanded)
355}
356
357/// A node in the view tree (during parsing).
358enum ViewNode {
359    /// An element like <Text>...</Text>
360    Element(ElementNode),
361    /// A string literal "Hello"
362    Text(String),
363    /// An expression in braces {expr}
364    Expr(Expr),
365}
366
367/// A prop like on_press={...} or selected={...}
368struct Prop {
369    name: Ident,
370    value: Expr,
371}
372
373struct ElementNode {
374    tag: String,
375    props: Vec<Prop>,
376    children: Vec<ViewNode>,
377}
378
379impl Parse for ViewNode {
380    fn parse(input: ParseStream) -> Result<Self> {
381        if input.peek(Token![<]) {
382            // Parse element: <Tag prop={val}>...</Tag>
383            input.parse::<Token![<]>()?;
384            let tag: Ident = input.parse()?;
385
386            // Parse props
387            let mut props = Vec::new();
388            while !input.peek(Token![>]) && !input.peek(Token![/]) {
389                let name: Ident = input.parse()?;
390                input.parse::<Token![=]>()?;
391                let content;
392                braced!(content in input);
393                let value: Expr = content.parse()?;
394                props.push(Prop { name, value });
395            }
396
397            // Check for self-closing tag: <Tag />
398            if input.peek(Token![/]) {
399                input.parse::<Token![/]>()?;
400                input.parse::<Token![>]>()?;
401                return Ok(ViewNode::Element(ElementNode {
402                    tag: tag.to_string(),
403                    props,
404                    children: Vec::new(),
405                }));
406            }
407
408            input.parse::<Token![>]>()?;
409
410            let mut children = Vec::new();
411
412            // Parse children until we hit the closing tag
413            while !(input.peek(Token![<]) && input.peek2(Token![/])) {
414                if input.is_empty() {
415                    return Err(syn::Error::new(
416                        tag.span(),
417                        format!("Unclosed tag: <{}>", tag),
418                    ));
419                }
420                children.push(input.parse()?);
421            }
422
423            // Parse closing tag: </Tag>
424            input.parse::<Token![<]>()?;
425            input.parse::<Token![/]>()?;
426            let close_tag: Ident = input.parse()?;
427            input.parse::<Token![>]>()?;
428
429            if tag != close_tag {
430                return Err(syn::Error::new(
431                    close_tag.span(),
432                    format!(
433                        "Mismatched tags: expected </{}>, found </{}>",
434                        tag, close_tag
435                    ),
436                ));
437            }
438
439            Ok(ViewNode::Element(ElementNode {
440                tag: tag.to_string(),
441                props,
442                children,
443            }))
444        } else if input.peek(LitStr) {
445            // Parse string literal: "Hello"
446            let lit: LitStr = input.parse()?;
447            Ok(ViewNode::Text(lit.value()))
448        } else if input.peek(syn::token::Brace) {
449            // Parse expression: {expr}
450            let content;
451            braced!(content in input);
452            let expr: Expr = content.parse()?;
453            Ok(ViewNode::Expr(expr))
454        } else {
455            Err(input.error("Expected <Element>, \"string literal\", or {expression}"))
456        }
457    }
458}
459
460impl ViewNode {
461    fn to_tokens(&self) -> TokenStream2 {
462        match self {
463            ViewNode::Text(s) => {
464                quote! { telex::View::text(#s) }
465            }
466            ViewNode::Expr(expr) => {
467                // Convert expression to string for text
468                quote! { telex::View::text(format!("{}", #expr)) }
469            }
470            ViewNode::Element(elem) => elem.to_tokens(),
471        }
472    }
473}
474
475impl ElementNode {
476    fn to_tokens(&self) -> TokenStream2 {
477        match self.tag.as_str() {
478            "Text" => {
479                // <Text>"content"</Text> or <Text>{expr}</Text>
480                if let Some(child) = self.children.first() {
481                    match child {
482                        ViewNode::Text(content) => quote! { telex::View::text(#content) },
483                        ViewNode::Expr(expr) => quote! { telex::View::text(format!("{}", #expr)) },
484                        _ => quote! { telex::View::text("") },
485                    }
486                } else {
487                    quote! { telex::View::text("") }
488                }
489            }
490            "VStack" => {
491                let mut builder_calls = Vec::new();
492
493                // Handle props (spacing)
494                for prop in &self.props {
495                    let name = &prop.name;
496                    let value = &prop.value;
497                    builder_calls.push(quote! { .#name(#value) });
498                }
499
500                // Handle children
501                for child in &self.children {
502                    let tokens = child.to_tokens();
503                    builder_calls.push(quote! { .child(#tokens) });
504                }
505
506                quote! { telex::View::vstack()#(#builder_calls)*.build() }
507            }
508            "HStack" => {
509                let mut builder_calls = Vec::new();
510
511                // Handle props (spacing)
512                for prop in &self.props {
513                    let name = &prop.name;
514                    let value = &prop.value;
515                    builder_calls.push(quote! { .#name(#value) });
516                }
517
518                // Handle children
519                for child in &self.children {
520                    let tokens = child.to_tokens();
521                    builder_calls.push(quote! { .child(#tokens) });
522                }
523
524                quote! { telex::View::hstack()#(#builder_calls)*.build() }
525            }
526            "Box" => {
527                let mut builder_calls = Vec::new();
528
529                // Handle props (border, padding, flex)
530                for prop in &self.props {
531                    let name = &prop.name;
532                    let value = &prop.value;
533                    builder_calls.push(quote! { .#name(#value) });
534                }
535
536                // Handle single child
537                if let Some(child) = self.children.first() {
538                    let tokens = child.to_tokens();
539                    builder_calls.push(quote! { .child(#tokens) });
540                }
541
542                quote! { telex::View::boxed()#(#builder_calls)*.build() }
543            }
544            "Spacer" => {
545                // Spacer with optional flex prop
546                if let Some(prop) = self.props.iter().find(|p| p.name == "flex") {
547                    let value = &prop.value;
548                    quote! { telex::View::spacer_flex(#value) }
549                } else {
550                    quote! { telex::View::spacer() }
551                }
552            }
553            "Button" => {
554                // Parse props and children for Button
555                let mut builder_calls = Vec::new();
556
557                // Handle props
558                for prop in &self.props {
559                    let name = &prop.name;
560                    let value = &prop.value;
561                    builder_calls.push(quote! { .#name(#value) });
562                }
563
564                // Handle label from children
565                if let Some(child) = self.children.first() {
566                    match child {
567                        ViewNode::Text(label) => {
568                            builder_calls.push(quote! { .label(#label) });
569                        }
570                        ViewNode::Expr(expr) => {
571                            builder_calls.push(quote! { .label(format!("{}", #expr)) });
572                        }
573                        _ => {}
574                    }
575                }
576
577                quote! { telex::View::button()#(#builder_calls)*.build() }
578            }
579            "List" => {
580                // Parse props for List: items, selected, on_select
581                let mut builder_calls = Vec::new();
582
583                for prop in &self.props {
584                    let name = &prop.name;
585                    let value = &prop.value;
586                    builder_calls.push(quote! { .#name(#value) });
587                }
588
589                quote! { telex::View::list()#(#builder_calls)*.build() }
590            }
591            "TextInput" => {
592                // Parse props for TextInput: value, placeholder, on_change
593                let mut builder_calls = Vec::new();
594
595                for prop in &self.props {
596                    let name = &prop.name;
597                    let value = &prop.value;
598                    builder_calls.push(quote! { .#name(#value) });
599                }
600
601                quote! { telex::View::text_input()#(#builder_calls)*.build() }
602            }
603            "Checkbox" => {
604                // Parse props and children for Checkbox: checked, on_toggle
605                let mut builder_calls = Vec::new();
606
607                // Handle props
608                for prop in &self.props {
609                    let name = &prop.name;
610                    let value = &prop.value;
611                    builder_calls.push(quote! { .#name(#value) });
612                }
613
614                // Handle label from children
615                if let Some(child) = self.children.first() {
616                    match child {
617                        ViewNode::Text(label) => {
618                            builder_calls.push(quote! { .label(#label) });
619                        }
620                        ViewNode::Expr(expr) => {
621                            builder_calls.push(quote! { .label(format!("{}", #expr)) });
622                        }
623                        _ => {}
624                    }
625                }
626
627                quote! { telex::View::checkbox()#(#builder_calls)*.build() }
628            }
629            "TextArea" => {
630                // Parse props for TextArea: value, placeholder, rows, cursor_line, cursor_col, on_change
631                let mut builder_calls = Vec::new();
632
633                for prop in &self.props {
634                    let name = &prop.name;
635                    let value = &prop.value;
636                    builder_calls.push(quote! { .#name(#value) });
637                }
638
639                quote! { telex::View::text_area()#(#builder_calls)*.build() }
640            }
641            "Modal" => {
642                // Parse props for Modal: visible, title, width, height, on_dismiss
643                let mut builder_calls = Vec::new();
644
645                for prop in &self.props {
646                    let name = &prop.name;
647                    let value = &prop.value;
648                    builder_calls.push(quote! { .#name(#value) });
649                }
650
651                // Handle single child
652                if let Some(child) = self.children.first() {
653                    let tokens = child.to_tokens();
654                    builder_calls.push(quote! { .child(#tokens) });
655                }
656
657                quote! { telex::View::modal()#(#builder_calls)*.build() }
658            }
659            "StyledText" => {
660                // Parse props for styled text: bold, italic, underline, dim, color, bg
661                let mut content = quote! { "" };
662                let mut bold_val = quote! { false };
663                let mut italic_val = quote! { false };
664                let mut underline_val = quote! { false };
665                let mut dim_val = quote! { false };
666                let mut color_call = quote! {};
667                let mut bg_call = quote! {};
668
669                // Handle props
670                for prop in &self.props {
671                    let name_str = prop.name.to_string();
672                    let value = &prop.value;
673
674                    match name_str.as_str() {
675                        "bold" => bold_val = quote! { #value },
676                        "italic" => italic_val = quote! { #value },
677                        "underline" => underline_val = quote! { #value },
678                        "dim" => dim_val = quote! { #value },
679                        "color" => color_call = quote! { .color(#value) },
680                        "bg" => bg_call = quote! { .bg(#value) },
681                        _ => {}
682                    }
683                }
684
685                // Handle text content from children
686                if let Some(child) = self.children.first() {
687                    match child {
688                        ViewNode::Text(text) => {
689                            content = quote! { #text };
690                        }
691                        ViewNode::Expr(expr) => {
692                            content = quote! { format!("{}", #expr) };
693                        }
694                        _ => {}
695                    }
696                }
697
698                // Generate conditional builder chain
699                quote! {
700                    {
701                        let __builder = telex::View::styled_text(#content);
702                        let __builder = if #bold_val { __builder.bold() } else { __builder };
703                        let __builder = if #italic_val { __builder.italic() } else { __builder };
704                        let __builder = if #underline_val { __builder.underline() } else { __builder };
705                        let __builder = if #dim_val { __builder.dim() } else { __builder };
706                        __builder #color_call #bg_call .build()
707                    }
708                }
709            }
710            unknown => {
711                // Provide helpful error with suggestions
712                let known_elements = [
713                    "Text",
714                    "StyledText",
715                    "VStack",
716                    "HStack",
717                    "Box",
718                    "Spacer",
719                    "Button",
720                    "List",
721                    "TextInput",
722                    "TextArea",
723                    "Checkbox",
724                    "Modal",
725                ];
726
727                // Find similar element names (simple edit distance check)
728                let suggestion = known_elements
729                    .iter()
730                    .find(|&e| {
731                        let e_lower = e.to_lowercase();
732                        let u_lower = unknown.to_lowercase();
733                        e_lower.starts_with(&u_lower[..1.min(u_lower.len())])
734                            || u_lower.starts_with(&e_lower[..1.min(e_lower.len())])
735                            || e_lower.contains(&u_lower)
736                            || u_lower.contains(&e_lower)
737                    });
738
739                let msg = if let Some(suggested) = suggestion {
740                    format!(
741                        "Unknown element: <{}>. Did you mean <{}>?\n\nAvailable elements: {}",
742                        unknown,
743                        suggested,
744                        known_elements.join(", ")
745                    )
746                } else {
747                    format!(
748                        "Unknown element: <{}>.\n\nAvailable elements: {}",
749                        unknown,
750                        known_elements.join(", ")
751                    )
752                };
753                quote! { compile_error!(#msg) }
754            }
755        }
756    }
757}