Skip to main content

oxframe_macros/
lib.rs

1//! Macros re-exported by the `oxframe` facade. Generated code refers to
2//! `::oxframe`, so they are meant to be used through it.
3
4#![warn(missing_docs)]
5
6use proc_macro::TokenStream;
7use quote::{format_ident, quote};
8use syn::parse::{Parse, ParseStream};
9use syn::punctuated::Punctuated;
10use syn::{Expr, ItemFn, LitStr, Token, parse_macro_input};
11
12fn valid_name(name: &str) -> bool {
13    !name.is_empty()
14        && name
15            .bytes()
16            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-')
17}
18
19fn check_name(lit: &LitStr) -> syn::Result<()> {
20    if valid_name(&lit.value()) {
21        Ok(())
22    } else {
23        Err(syn::Error::new(
24            lit.span(),
25            "names of markers, eases and tunables must match [a-z0-9_-]+",
26        ))
27    }
28}
29
30/// Turns `async fn intro(view: View) { .. }` into `fn intro() -> SceneDef`.
31///
32/// The scene's name is the function name; its meta lives in
33/// `<name>.meta.json` next to the source file.
34#[proc_macro_attribute]
35pub fn scene(attr: TokenStream, item: TokenStream) -> TokenStream {
36    if !attr.is_empty() {
37        return syn::Error::new(
38            proc_macro2::TokenStream::from(attr)
39                .into_iter()
40                .next()
41                .unwrap()
42                .span(),
43            "#[scene] takes no arguments",
44        )
45        .to_compile_error()
46        .into();
47    }
48    let func = parse_macro_input!(item as ItemFn);
49    match expand_scene(func) {
50        Ok(ts) => ts.into(),
51        Err(e) => e.to_compile_error().into(),
52    }
53}
54
55fn expand_scene(func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
56    let sig = &func.sig;
57    if sig.asyncness.is_none() {
58        return Err(syn::Error::new_spanned(
59            sig.fn_token,
60            "a scene must be an `async fn`",
61        ));
62    }
63    if !sig.generics.params.is_empty() {
64        return Err(syn::Error::new_spanned(
65            &sig.generics,
66            "a scene cannot be generic",
67        ));
68    }
69    if sig.inputs.len() > 1 {
70        return Err(syn::Error::new_spanned(
71            &sig.inputs,
72            "a scene takes at most one argument: `view: View`",
73        ));
74    }
75    let name = &sig.ident;
76    let name_str = name.to_string();
77    if !valid_name(&name_str) {
78        return Err(syn::Error::new_spanned(
79            name,
80            "scene names must be lowercase snake_case ([a-z0-9_])",
81        ));
82    }
83    let vis = &func.vis;
84    let attrs = &func.attrs;
85    let inner = format_ident!("__oxframe_scene_{}", name);
86    let mut inner_fn = func.clone();
87    inner_fn.sig.ident = inner.clone();
88    inner_fn.vis = syn::Visibility::Inherited;
89    inner_fn.attrs.clear();
90    let call = if sig.inputs.is_empty() {
91        quote!(#inner())
92    } else {
93        quote!(#inner(view))
94    };
95
96    Ok(quote! {
97        #(#attrs)*
98        #vis fn #name() -> ::oxframe::SceneDef {
99            #inner_fn
100            ::oxframe::SceneDef::new(
101                #name_str,
102                ::oxframe::SourceLocation {
103                    file: ::core::file!(),
104                    manifest_dir: ::core::env!("CARGO_MANIFEST_DIR"),
105                },
106                |view: ::oxframe::View| -> ::oxframe::BoxFuture {
107                    let _ = &view;
108                    ::std::boxed::Box::pin(#call)
109                },
110            )
111        }
112    })
113}
114
115/// `color!("#e13238")` — a `Color` checked at compile time. Accepts
116/// `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa`.
117#[proc_macro]
118pub fn color(input: TokenStream) -> TokenStream {
119    let lit = parse_macro_input!(input as LitStr);
120    let s = lit.value();
121    let hex = s.strip_prefix('#').unwrap_or(&s);
122    let digits: Option<Vec<u8>> = hex
123        .chars()
124        .map(|c| c.to_digit(16).map(|d| d as u8))
125        .collect();
126    let channels = match (digits, hex.len()) {
127        (Some(d), 3 | 4) => Some(d.iter().map(|v| v * 17).collect::<Vec<_>>()),
128        (Some(d), 6 | 8) => Some(d.chunks(2).map(|p| p[0] * 16 + p[1]).collect()),
129        _ => None,
130    };
131    let Some(mut c) = channels else {
132        return syn::Error::new(lit.span(), "expected #rgb, #rgba, #rrggbb or #rrggbbaa")
133            .to_compile_error()
134            .into();
135    };
136    if c.len() == 3 {
137        c.push(255);
138    }
139    let (r, g, b, a) = (c[0], c[1], c[2], c[3]);
140    quote!(::oxframe::Color::from_rgba8(#r, #g, #b, #a)).into()
141}
142
143struct NameArgs {
144    name: LitStr,
145    rest: Punctuated<Expr, Token![,]>,
146}
147
148impl Parse for NameArgs {
149    fn parse(input: ParseStream) -> syn::Result<Self> {
150        let name: LitStr = input.parse()?;
151        check_name(&name)?;
152        let rest = if input.is_empty() {
153            Punctuated::new()
154        } else {
155            input.parse::<Token![,]>()?;
156            Punctuated::parse_terminated(input)?
157        };
158        Ok(Self { name, rest })
159    }
160}
161
162/// `ease!("slide")` or `ease!("slide", presets::OUT_BACK)` — a named Bézier
163/// easing editable in the curve editor; the value lives in `meta.json`.
164#[proc_macro]
165pub fn ease(input: TokenStream) -> TokenStream {
166    let NameArgs { name, rest } = parse_macro_input!(input as NameArgs);
167    match rest.len() {
168        0 => quote!(::oxframe::ease_named(#name, ::core::option::Option::None)).into(),
169        1 => {
170            let d = &rest[0];
171            quote!(::oxframe::ease_named(#name, ::core::option::Option::Some(#d))).into()
172        }
173        _ => syn::Error::new_spanned(&rest, "usage: ease!(\"name\") or ease!(\"name\", default)")
174            .to_compile_error()
175            .into(),
176    }
177}
178
179/// `tune!("speed", 1.4)` — a number editable with a slider in the
180/// inspector; the value lives in `meta.json`.
181#[proc_macro]
182pub fn tune(input: TokenStream) -> TokenStream {
183    let NameArgs { name, rest } = parse_macro_input!(input as NameArgs);
184    if rest.len() != 1 {
185        return syn::Error::new(name.span(), "usage: tune!(\"name\", default)")
186            .to_compile_error()
187            .into();
188    }
189    let d = &rest[0];
190    quote!(::oxframe::tunable(#name, (#d) as f64)).into()
191}