1#![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#[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#[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#[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#[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}