whisker_macros/lib.rs
1//! Procedural macros for Whisker.
2//!
3//! - [`main`] — designates the user's app entry. Generates the
4//! `whisker_app_main` and `whisker_tick` FFI exports the native
5//! host calls into; the user writes `fn app() -> Element`.
6//! - [`render!`] — fine-grained renderer macro. Emits imperative
7//! `view::*` dispatch + `effect`s for dynamic parts. See
8//! `crates/whisker-macros/src/render.rs` for the grammar.
9//! - [`component`] — wraps a function so it runs inside a fresh
10//! reactive owner. The owner is registered against the function's
11//! fn pointer so the hot-reload remount path can find it. See
12//! `docs/reactivity-design.md`.
13
14use proc_macro::TokenStream;
15use quote::quote;
16use syn::{parse_macro_input, ItemFn};
17
18mod component;
19mod css;
20mod module_component;
21mod render;
22
23/// Annotates the user's app function (returning `whisker::Element`) and
24/// generates the FFI symbols the iOS/Android host expects.
25///
26/// ```ignore
27/// use whisker::prelude::*;
28///
29/// #[whisker::main]
30/// fn app() -> Element {
31/// render! { page { text(value: "Hello") } }
32/// }
33/// ```
34///
35/// Expands to (roughly):
36///
37/// ```ignore
38/// fn app() -> Element { /* user body */ }
39///
40/// #[no_mangle]
41/// pub extern "C" fn whisker_app_main(
42/// engine: *mut std::ffi::c_void,
43/// request_frame: Option<extern "C" fn(*mut std::ffi::c_void)>,
44/// request_frame_data: *mut std::ffi::c_void,
45/// ) {
46/// ::whisker::__main_runtime::run(engine, request_frame, request_frame_data, app);
47/// }
48///
49/// #[no_mangle]
50/// pub extern "C" fn whisker_tick(engine: *mut std::ffi::c_void) -> bool {
51/// ::whisker::__main_runtime::tick(engine)
52/// }
53/// ```
54///
55/// `request_frame` is the host's "wake up the render loop" callback. The
56/// runtime invokes it when a signal update marks the tree dirty so the
57/// host can unpause its `CADisplayLink` (or equivalent) to schedule the
58/// next tick. Pass `None` to opt into an unconditional 60Hz loop.
59///
60/// `whisker_tick` returns `true` when the runtime is idle after the tick;
61/// the host can pause its render loop until the next `request_frame`
62/// fires.
63#[proc_macro_attribute]
64pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
65 let func = parse_macro_input!(item as ItemFn);
66 let fn_name = &func.sig.ident;
67
68 let expanded = quote! {
69 #func
70
71 // The app fn the runtime invokes every frame. Unconditionally
72 // routes through `whisker::__main_runtime::call_user_app`, which
73 // is `#[inline(always)]` so the wrapper body lands in the user
74 // crate's compilation unit. Whether the wrapper actually
75 // dispatches through `subsecond::call` (Tier 1 / hot-reload
76 // on) or just invokes `#fn_name()` directly (release) is
77 // decided by `whisker`'s own `hot-reload` feature flag — the
78 // user crate doesn't need a matching feature of its own.
79 fn __whisker_app_dispatch() -> ::whisker::runtime::view::Element {
80 ::whisker::__main_runtime::call_user_app(#fn_name)
81 }
82
83 #[no_mangle]
84 pub extern "C" fn whisker_app_main(
85 engine: *mut ::std::ffi::c_void,
86 request_frame: ::std::option::Option<
87 extern "C" fn(*mut ::std::ffi::c_void),
88 >,
89 request_frame_data: *mut ::std::ffi::c_void,
90 ) {
91 ::whisker::__main_runtime::run(
92 engine,
93 request_frame,
94 request_frame_data,
95 __whisker_app_dispatch,
96 );
97 }
98
99 #[no_mangle]
100 pub extern "C" fn whisker_tick(engine: *mut ::std::ffi::c_void) -> bool {
101 ::whisker::__main_runtime::tick(engine)
102 }
103
104 // Anchor symbol used by Whisker's vendored subsecond fork to
105 // compute the ASLR slide between this dylib's static layout
106 // (cached server-side) and its runtime load address. Both the
107 // host dylib and every patch dylib must export this so
108 // `dlsym(RTLD_DEFAULT, "whisker_aslr_anchor")` resolves
109 // unambiguously inside the user's `.so`.
110 //
111 // Why a unique name instead of `main` (upstream subsecond's
112 // sentinel): on Android, Whisker is loaded via
113 // `System.loadLibrary` into a process whose linker namespace
114 // already contains several `main` symbols
115 // (`app_process64`'s, plus any prior memfd patches), so a
116 // dlsym for `main` returns the wrong one and the slide math
117 // computes garbage. A unique name only exists in the user's
118 // `.so`, so the lookup is collision-free regardless of
119 // namespace order.
120 //
121 // The stub never runs — Whisker is JNI-loaded, never executed
122 // as a process entry point. It only needs to exist in the
123 // export list at a known static address.
124 #[no_mangle]
125 pub extern "C" fn whisker_aslr_anchor() -> ::std::ffi::c_int { 0 }
126 };
127
128 expanded.into()
129}
130
131/// Fine-grained renderer macro. Emits imperative element-creation
132/// code that calls into [`whisker::runtime::view`] through the
133/// thread-local installed renderer, and returns an [`Element`].
134///
135/// ```ignore
136/// use whisker::prelude::*;
137///
138/// let handle = render! {
139/// view(
140/// style: "padding: 16px;",
141/// on_tap: move |_| println!("tapped"),
142/// ) {
143/// text(value: "Hello, world")
144/// }
145/// };
146/// ```
147///
148/// See `crates/whisker-macros/src/render.rs` for the full kwarg
149/// grammar. Dynamic values flow through `Signal<T>` props; bare
150/// `{expr}` blocks inside a children list are rejected (use
151/// `text(value: <expr>)` instead).
152#[proc_macro]
153pub fn render(input: TokenStream) -> TokenStream {
154 render::expand(input)
155}
156
157/// `css!(name: value, …)` — kwarg syntax for the [`Css`] builder.
158///
159/// Lowers to a [`Css::new()`] method chain (`Css::new().name(value)
160/// .…`). `Css` is taken from the call site's scope, so
161/// `use whisker::prelude::*` (which re-exports `Css`) is the only
162/// import callers need.
163///
164/// The proc-macro implementation tolerates partial input from
165/// rust-analyzer's completion engine: a kwarg whose value hasn't
166/// been typed yet (`css!(back|`) is expanded as
167/// `.<name>(())` so RA still sees a real method-call site and
168/// fires its method-name completion. The unit `()` is intentionally
169/// type-incorrect; the program already doesn't compile while the
170/// user is mid-typing.
171///
172/// ```ignore
173/// use whisker::prelude::*;
174///
175/// let s = css!(
176/// background_color: Color::hex(0x1A1330),
177/// padding: (px(8), px(16)),
178/// border: Border::new().width(px(1)).style(BorderStyle::Solid),
179/// );
180/// ```
181///
182/// [`Css`]: whisker_css::Css
183/// [`Css::new()`]: whisker_css::Css::new
184#[proc_macro]
185pub fn css(input: TokenStream) -> TokenStream {
186 css::expand(input.into()).into()
187}
188
189/// Mark a function as a Whisker reactive component.
190///
191/// The macro takes the user's `fn xxx(a: A, b: B) -> Element`
192/// and emits both:
193///
194/// 1. A `XxxProps` struct (Pascal-cased function name + `Props`)
195/// derived from the parameter list, plus a hand-rolled
196/// `XxxPropsBuilder` so callers can construct Props via
197/// `XxxProps::builder().a(...).b(...).build()`.
198/// Each setter accepts `impl Into<T>` for `Into` coercion on the
199/// call side (`&str` → `String`, `i32` → `f64`, …).
200/// `Option<T>` props get a strip-option setter (accept the inner
201/// `T`) and default to `None` when omitted. `Children` props get
202/// a default empty closure. A `#[prop(default = expr)]` attribute
203/// on a parameter inserts `expr` as the field's default at `.build()`.
204/// Required fields that the user didn't set panic at `.build()` with
205/// `"required field `xxx` was not set"`.
206///
207/// 2. A rewritten `fn xxx(__props: XxxProps) -> Element` whose
208/// body destructures the props back into local variables and runs
209/// the user's original `#block` inside the existing
210/// `mount_component_remountable` machinery (per-component
211/// remount + subsecond hot-reload integration).
212///
213/// The signature change is deliberate: positional `xxx(a, b)`
214/// invocations no longer compile. User components are now invoked
215/// exclusively through `render!`'s `xxx { a: …, b: … }` syntax,
216/// which the `render!` macro lowers to
217/// `xxx(XxxProps::builder().a(…).b(…).build())`. This unifies the
218/// call-site shape with built-in elements (`view { … }`).
219///
220/// ```ignore
221/// use whisker::prelude::*;
222///
223/// #[component]
224/// fn counter(initial: i32) -> Element {
225/// let (count, set_count) = signal(initial);
226/// render! { /* ... */ }
227/// }
228///
229/// // Call site (always through `render!`):
230/// render! { counter { initial: 0 } }
231/// ```
232#[proc_macro_attribute]
233pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
234 component::expand(item.into()).into()
235}
236
237/// Declare a Whisker-side wrapper for a Lynx-registered view module's element.
238///
239/// ```ignore
240/// #[whisker::module_component("Hello")]
241/// pub fn hello(style: Signal<String>) -> Element;
242/// ```
243///
244/// Generates the same Props + builder + PascalCase-alias surface as
245/// `#[component]`, but the function body is **auto-generated**: it
246/// calls `view::create_element_by_name(tag)` and then applies each
247/// declared prop as either an inline-style (for the `style` prop) or
248/// a SetAttribute (everything else, kebab-cased). Static vs reactive
249/// dispatch goes through the same `apply_styles` / `apply_attr`
250/// helpers built-in tags use, so a `Signal::Dynamic` prop transparently
251/// effect-wraps the attribute write.
252///
253/// The tag string passed to Lynx at runtime is
254/// `<cargo-crate-name>:<attr-tag>` — the macro auto-prepends
255/// `env!("CARGO_PKG_NAME")` so two unrelated module packages can
256/// both declare a component named `Hello` without colliding in
257/// Lynx's behaviour registry. The matching platform-side
258/// `@WhiskerModule` DSL `Name(...)` is namespaced the same way by
259/// the per-platform codegen.
260///
261/// Imperative methods on a mounted element are dispatched through
262/// the element's `ElementRef` (`ref:` prop) via
263/// `ElementRef::invoke(method, args)` — there is no separate
264/// element-method declaration macro.
265///
266/// Call-site shape mirrors built-in tags + user components:
267///
268/// ```ignore
269/// render! {
270/// Hello(style: "width: 100%; height: 8px;")
271/// }
272/// ```
273///
274/// See `crates/whisker-macros/src/module_component.rs` for the
275/// emission details (children props are NOT yet supported; tracked
276/// in follow-ups).
277#[proc_macro_attribute]
278pub fn module_component(attr: TokenStream, item: TokenStream) -> TokenStream {
279 module_component::expand(attr.into(), item.into()).into()
280}