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//! > **Status:** oxframe is under very active development. The API is not stable yet and
5//! > may change from version to version. Pin the exact version in your Cargo.toml and check
6//! > the [changelog](https://github.com/oxygenframe/oxframe-sdk/blob/main/CHANGELOG.md)
7//! > before upgrading.
8
9#![warn(missing_docs)]
10
11use proc_macro::TokenStream;
12use quote::{format_ident, quote};
13use syn::parse::{Parse, ParseStream};
14use syn::punctuated::Punctuated;
15use syn::{Expr, ItemFn, LitStr, Token, parse_macro_input};
16
17fn valid_name(name: &str) -> bool {
18    !name.is_empty()
19        && name
20            .bytes()
21            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-')
22}
23
24fn check_name(lit: &LitStr) -> syn::Result<()> {
25    if valid_name(&lit.value()) {
26        Ok(())
27    } else {
28        Err(syn::Error::new(
29            lit.span(),
30            "names of markers, eases and tunables must match [a-z0-9_-]+",
31        ))
32    }
33}
34
35/// Turns `async fn intro(view: View) { .. }` into `fn intro() -> SceneDef`.
36///
37/// The scene's name is the function name; its meta lives in
38/// `<name>.meta.json` next to the source file.
39#[proc_macro_attribute]
40pub fn scene(attr: TokenStream, item: TokenStream) -> TokenStream {
41    if !attr.is_empty() {
42        return syn::Error::new(
43            proc_macro2::TokenStream::from(attr)
44                .into_iter()
45                .next()
46                .unwrap()
47                .span(),
48            "#[scene] takes no arguments",
49        )
50        .to_compile_error()
51        .into();
52    }
53    let func = parse_macro_input!(item as ItemFn);
54    match expand_scene(func) {
55        Ok(ts) => ts.into(),
56        Err(e) => e.to_compile_error().into(),
57    }
58}
59
60fn expand_scene(func: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
61    let sig = &func.sig;
62    if sig.asyncness.is_none() {
63        return Err(syn::Error::new_spanned(
64            sig.fn_token,
65            "a scene must be an `async fn`",
66        ));
67    }
68    if !sig.generics.params.is_empty() {
69        return Err(syn::Error::new_spanned(
70            &sig.generics,
71            "a scene cannot be generic",
72        ));
73    }
74    if sig.inputs.len() > 1 {
75        return Err(syn::Error::new_spanned(
76            &sig.inputs,
77            "a scene takes at most one argument: `view: View`",
78        ));
79    }
80    let name = &sig.ident;
81    let name_str = name.to_string();
82    if !valid_name(&name_str) {
83        return Err(syn::Error::new_spanned(
84            name,
85            "scene names must be lowercase snake_case ([a-z0-9_])",
86        ));
87    }
88    let vis = &func.vis;
89    let attrs = &func.attrs;
90    let inner = format_ident!("__oxframe_scene_{}", name);
91    let mut inner_fn = func.clone();
92    inner_fn.sig.ident = inner.clone();
93    inner_fn.vis = syn::Visibility::Inherited;
94    inner_fn.attrs.clear();
95    let call = if sig.inputs.is_empty() {
96        quote!(#inner())
97    } else {
98        quote!(#inner(view))
99    };
100
101    Ok(quote! {
102        #(#attrs)*
103        #vis fn #name() -> ::oxframe::SceneDef {
104            #inner_fn
105            ::oxframe::SceneDef::new(
106                #name_str,
107                ::oxframe::SourceLocation {
108                    file: ::core::file!(),
109                    manifest_dir: ::core::env!("CARGO_MANIFEST_DIR"),
110                },
111                |view: ::oxframe::View| -> ::oxframe::BoxFuture {
112                    let _ = &view;
113                    ::std::boxed::Box::pin(#call)
114                },
115            )
116        }
117    })
118}
119
120/// `color!("#e13238")` — a `Color` checked at compile time. Accepts
121/// `#rgb`, `#rgba`, `#rrggbb` and `#rrggbbaa`.
122#[proc_macro]
123pub fn color(input: TokenStream) -> TokenStream {
124    let lit = parse_macro_input!(input as LitStr);
125    let s = lit.value();
126    let hex = s.strip_prefix('#').unwrap_or(&s);
127    let digits: Option<Vec<u8>> = hex
128        .chars()
129        .map(|c| c.to_digit(16).map(|d| d as u8))
130        .collect();
131    let channels = match (digits, hex.len()) {
132        (Some(d), 3 | 4) => Some(d.iter().map(|v| v * 17).collect::<Vec<_>>()),
133        (Some(d), 6 | 8) => Some(d.chunks(2).map(|p| p[0] * 16 + p[1]).collect()),
134        _ => None,
135    };
136    let Some(mut c) = channels else {
137        return syn::Error::new(lit.span(), "expected #rgb, #rgba, #rrggbb or #rrggbbaa")
138            .to_compile_error()
139            .into();
140    };
141    if c.len() == 3 {
142        c.push(255);
143    }
144    let (r, g, b, a) = (c[0], c[1], c[2], c[3]);
145    quote!(::oxframe::Color::from_rgba8(#r, #g, #b, #a)).into()
146}
147
148struct NameArgs {
149    name: LitStr,
150    rest: Punctuated<Expr, Token![,]>,
151}
152
153impl Parse for NameArgs {
154    fn parse(input: ParseStream) -> syn::Result<Self> {
155        let name: LitStr = input.parse()?;
156        check_name(&name)?;
157        let rest = if input.is_empty() {
158            Punctuated::new()
159        } else {
160            input.parse::<Token![,]>()?;
161            Punctuated::parse_terminated(input)?
162        };
163        Ok(Self { name, rest })
164    }
165}
166
167/// `ease!("slide")` or `ease!("slide", presets::OUT_BACK)` — a named Bézier
168/// easing editable in the curve editor; the value lives in `meta.json`.
169#[proc_macro]
170pub fn ease(input: TokenStream) -> TokenStream {
171    let NameArgs { name, rest } = parse_macro_input!(input as NameArgs);
172    match rest.len() {
173        0 => quote!(::oxframe::ease_named(#name, ::core::option::Option::None)).into(),
174        1 => {
175            let d = &rest[0];
176            quote!(::oxframe::ease_named(#name, ::core::option::Option::Some(#d))).into()
177        }
178        _ => syn::Error::new_spanned(&rest, "usage: ease!(\"name\") or ease!(\"name\", default)")
179            .to_compile_error()
180            .into(),
181    }
182}
183
184/// `tune!("speed", 1.4)` — a number editable with a slider in the
185/// inspector; the value lives in `meta.json`.
186#[proc_macro]
187pub fn tune(input: TokenStream) -> TokenStream {
188    let NameArgs { name, rest } = parse_macro_input!(input as NameArgs);
189    if rest.len() != 1 {
190        return syn::Error::new(name.span(), "usage: tune!(\"name\", default)")
191            .to_compile_error()
192            .into();
193    }
194    let d = &rest[0];
195    quote!(::oxframe::tunable(#name, (#d) as f64)).into()
196}
197
198/// A node of your own from a struct that implements `Draw`.
199///
200/// Fields marked `#[signal]` become animatable: the builder gets a setter
201/// of the same name, and the handle (`<Name>Handle`) a method that returns
202/// the field's `Signal`. Other fields are set once on the builder. Every
203/// field type needs `Default` and `Clone`; signal fields also `Lerp`.
204///
205/// ```ignore
206/// #[derive(Node)]
207/// pub struct Gauge {
208///     #[signal] value: f64,
209///     #[signal] size: f64,
210/// }
211///
212/// impl Draw for Gauge {
213///     fn draw(&self, ctx: &mut DrawCtx) {
214///         ctx.arc((0.0, 0.0), self.size / 2.0, 135.0, 270.0 * self.value).stroke(Accent, 16.0);
215///     }
216///     fn size(&self) -> (f64, f64) { (self.size, self.size) }
217/// }
218///
219/// let g = view.add(Gauge::new().size(200.0));   // GaugeHandle
220/// g.value().to(0.8, 1.0).await;
221/// ```
222#[proc_macro_derive(Node, attributes(signal))]
223pub fn derive_node(input: TokenStream) -> TokenStream {
224    let input = parse_macro_input!(input as syn::DeriveInput);
225    match expand_node(input) {
226        Ok(tokens) => tokens.into(),
227        Err(err) => err.to_compile_error().into(),
228    }
229}
230
231fn expand_node(input: syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
232    let name = &input.ident;
233    let vis = &input.vis;
234    if !input.generics.params.is_empty() {
235        return Err(syn::Error::new_spanned(
236            &input.generics,
237            "#[derive(Node)] does not support generic structs",
238        ));
239    }
240    let syn::Data::Struct(data) = &input.data else {
241        return Err(syn::Error::new_spanned(
242            name,
243            "#[derive(Node)] needs a struct",
244        ));
245    };
246    let syn::Fields::Named(fields) = &data.fields else {
247        return Err(syn::Error::new_spanned(
248            name,
249            "#[derive(Node)] needs a struct with named fields",
250        ));
251    };
252    let builder = format_ident!("{name}Builder");
253    let handle = format_ident!("{name}Handle");
254    let state = format_ident!("__{name}Signals");
255    let type_name = name.to_string();
256
257    let mut builder_fields = Vec::new();
258    let mut builder_defaults = Vec::new();
259    let mut setters = Vec::new();
260    let mut state_fields = Vec::new();
261    let mut state_init = Vec::new();
262    let mut snapshot = Vec::new();
263    let mut handle_methods = Vec::new();
264    let mut getters = Vec::new();
265    for f in &fields.named {
266        let ident = f.ident.as_ref().expect("named field");
267        let ty = &f.ty;
268        let signal = f.attrs.iter().any(|a| a.path().is_ident("signal"));
269        let doc = format!("Sets `{ident}`.");
270        builder_fields.push(quote! { #ident: #ty });
271        builder_defaults.push(quote! { #ident: ::core::default::Default::default() });
272        setters.push(quote! {
273            #[doc = #doc]
274            #vis fn #ident(mut self, v: impl ::core::convert::Into<#ty>) -> Self {
275                self.#ident = v.into();
276                self
277            }
278        });
279        if signal {
280            let label = format!("{name}.{ident}");
281            let hdoc = format!("`{ident}` as an animatable signal.");
282            let gdoc = format!("The current `{ident}`.");
283            state_fields.push(quote! { #ident: ::oxframe::Signal<#ty> });
284            state_init.push(quote! {
285                #ident: ::oxframe::Signal::new(self.#ident).with_name(#label)
286            });
287            snapshot.push(quote! { #ident: s.#ident.get() });
288            handle_methods.push(quote! {
289                #[doc = #hdoc]
290                #vis fn #ident(self) -> ::oxframe::Signal<#ty> {
291                    ::oxframe::nodes::node_api::state::<#state>(self.0).#ident.clone()
292                }
293            });
294            getters.push(quote! {
295                #[doc = #gdoc]
296                #vis fn #ident(&self) -> #ty {
297                    ::core::clone::Clone::clone(&self.#ident)
298                }
299            });
300        } else {
301            state_fields.push(quote! { #ident: #ty });
302            state_init.push(quote! { #ident: self.#ident });
303            snapshot.push(quote! { #ident: ::core::clone::Clone::clone(&s.#ident) });
304        }
305    }
306    let bdoc = format!("Builder for [`{name}`]; pass it to `add`.");
307    let hdoc = format!("Handle to a [`{name}`] node. Derefs to `Node`.");
308    Ok(quote! {
309        #[doc = #bdoc]
310        #[must_use = "a builder does nothing until it is passed to `add`"]
311        #vis struct #builder {
312            base: ::oxframe::nodes::BaseProps,
313            #(#builder_fields,)*
314        }
315
316        impl #name {
317            /// Starts a node with every field at its default.
318            #[allow(clippy::new_ret_no_self)]
319            #vis fn new() -> #builder {
320                #builder {
321                    base: ::core::default::Default::default(),
322                    #(#builder_defaults,)*
323                }
324            }
325            #(#getters)*
326        }
327
328        impl #builder {
329            #(#setters)*
330        }
331
332        #[doc(hidden)]
333        #vis struct #state {
334            #(#state_fields,)*
335        }
336
337        #[doc = #hdoc]
338        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
339        #vis struct #handle(::oxframe::Node);
340
341        impl #handle {
342            #(#handle_methods)*
343            /// The untyped node handle.
344            #vis fn node(self) -> ::oxframe::Node {
345                self.0
346            }
347        }
348
349        impl ::core::ops::Deref for #handle {
350            type Target = ::oxframe::Node;
351            fn deref(&self) -> &::oxframe::Node {
352                &self.0
353            }
354        }
355
356        impl ::core::convert::From<#handle> for ::oxframe::Node {
357            fn from(h: #handle) -> ::oxframe::Node {
358                h.0
359            }
360        }
361
362        impl ::oxframe::NodeBuilder for #builder {
363            type Handle = #handle;
364            fn base(&mut self) -> &mut ::oxframe::nodes::BaseProps {
365                &mut self.base
366            }
367            fn build(
368                self,
369                graph: &::oxframe::nodes::SceneGraph,
370                parent: ::oxframe::NodeId,
371            ) -> #handle {
372                let state = ::std::rc::Rc::new(#state { #(#state_init,)* });
373                let s = state.clone();
374                let snapshot: ::std::rc::Rc<dyn Fn() -> #name> =
375                    ::std::rc::Rc::new(move || #name { #(#snapshot,)* });
376                let id = ::oxframe::nodes::node_api::add(
377                    graph,
378                    parent,
379                    self.base,
380                    #type_name,
381                    snapshot,
382                    state,
383                );
384                #handle(::oxframe::Node::from_id(id))
385            }
386        }
387    })
388}