//! IR → Rust: the typed compile path. //! //! For a component `Counter` this emits: //! - `CounterProps` — one field per prop, `Default` if every prop has one; //! plain initial values that `mount` turns into signals; //! - `CounterCx` — the capability surface handlers receive: each prop as a //! `ReadSignal`, each state as a `Signal`, each output as an //! `Emitter` clone; //! - `trait CounterLogic` — one method per declared handler, so a missing //! or misspelled handler is a compile error at the `impl` site; //! - `struct Counter` with `mount(tree, parent, props, logic)` building the //! widget subtree, installing one reactive effect per binding, and wiring //! events to logic methods. //! //! All paths in generated code are rooted at `::guiduck`, the facade crate //! applications depend on. use std::collections::BTreeMap; use guiduck_component_core::ast::TypeKind; use guiduck_component_core::component_method_name; use guiduck_component_core::expr::{BinOp, Expr, Segment, UnOp}; use guiduck_component_core::ir::*; use guiduck_component_core::registry::{EventProp, PayloadKind, PropTy}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; /// Everything the host resolved that codegen must bake into the expansion but /// cannot work out for itself: the files read (for rebuild tracking), the /// three search paths a dev mount has to re-resolve against, each /// `(asset "…")` reference's absolute path, and the widget vocabulary the /// `.gdc` was compiled against. pub struct Host<'a> { /// Every file this compilation read, absolute — one `include_str!` each, /// so editing any of them rebuilds the expansion. pub track_paths: &'a [String], pub search_dirs: &'a [String], pub widget_dirs: &'a [String], pub asset_dirs: &'a [String], /// Each `(asset "…")` reference, by the path as written, to the absolute /// path it resolved to. pub assets: &'a BTreeMap, /// Where this component's own `.gdc` was found, absolute — the file /// `mount` watches when it is still there at run time. pub source_path: &'a str, } pub fn component(compiled: &Compiled, host: &Host<'_>) -> TokenStream { let Host { track_paths, assets, source_path, .. } = host; let ir = &compiled.root; let name = format_ident!("{}", ir.name); let props_name = format_ident!("{}Props", ir.name); let cx_name = format_ident!("{}Cx", ir.name); // A query handler's context is read-only over state: a query answers, it // does not act, so it may read state but not `set` it. The type exists only // when the component has a query handler. let query_cx_name = format_ident!("{}QueryCx", ir.name); let has_queries = ir.handlers.iter().any(|h| h.returns.is_some()); let logic_name = format_ident!("{}Logic", ir.name); let dyn_name = format_ident!("{}DynHandlers", ir.name); // One tracking const per file read (the component's own and every // transitively instantiated one, as absolute paths), so editing any of // them rebuilds this expansion. let tracking = track_paths.iter().map(|path| { quote! { const _: &str = ::core::include_str!(#path); } }); // --- Record structs --- let record_structs = ir.records.iter().map(|record| { let record_name = format_ident!("{}", record.name); let struct_fields = record.fields.iter().map(|(field, kind)| { let field = snake_ident(field); let ty = scalar_type_tokens(*kind); quote! { pub #field: #ty, } }); let to_dyn_fields = record.fields.iter().map(|(field, kind)| { let ident = snake_ident(field); let value = match kind { TypeKind::I32 | TypeKind::I64 => quote! { ::guiduck::core::component::DynValue::I64(self.#ident as i64) }, TypeKind::F32 | TypeKind::F64 => quote! { ::guiduck::core::component::DynValue::F64(self.#ident as f64) }, TypeKind::Bool => quote! { ::guiduck::core::component::DynValue::Bool(self.#ident) }, TypeKind::String => quote! { ::guiduck::core::component::DynValue::Str(self.#ident.clone()) }, }; quote! { (::std::string::String::from(#field), #value), } }); let from_dyn_fields = record.fields.iter().map(|(field, kind)| { let ident = snake_ident(field); let extract = match kind { TypeKind::I32 => quote! { match __field { ::core::option::Option::Some( ::guiduck::core::component::DynValue::I64(v), ) => *v as i32, _ => ::core::default::Default::default(), } }, TypeKind::I64 => quote! { match __field { ::core::option::Option::Some( ::guiduck::core::component::DynValue::I64(v), ) => *v, _ => ::core::default::Default::default(), } }, TypeKind::F32 => quote! { match __field { ::core::option::Option::Some( ::guiduck::core::component::DynValue::F64(v), ) => *v as f32, _ => ::core::default::Default::default(), } }, TypeKind::F64 => quote! { match __field { ::core::option::Option::Some( ::guiduck::core::component::DynValue::F64(v), ) => *v, _ => ::core::default::Default::default(), } }, TypeKind::Bool => quote! { match __field { ::core::option::Option::Some( ::guiduck::core::component::DynValue::Bool(v), ) => *v, _ => ::core::default::Default::default(), } }, TypeKind::String => quote! { match __field { ::core::option::Option::Some( ::guiduck::core::component::DynValue::Str(v), ) => v.clone(), _ => ::core::default::Default::default(), } }, }; quote! { #ident: { let __field = __fields .iter() .find(|(name, _)| name == #field) .map(|(_, value)| value); #extract }, } }); quote! { /// A `.gdc`-declared record. Field values round-trip the dev /// bridge as self-describing dynamic records. #[derive(Clone, Debug, PartialEq)] pub struct #record_name { #(#struct_fields)* } impl #record_name { #[doc(hidden)] pub fn __to_dyn(&self) -> ::guiduck::core::component::DynValue { ::guiduck::core::component::DynValue::Record(::std::vec![ #(#to_dyn_fields)* ]) } #[doc(hidden)] pub fn __from_dyn(value: &::guiduck::core::component::DynValue) -> Self { let __empty = ::std::vec::Vec::new(); let __fields = match value { ::guiduck::core::component::DynValue::Record(fields) => fields, _ => &__empty, }; Self { #(#from_dyn_fields)* } } } } }); // --- Props struct --- let prop_fields = ir.props.iter().map(|p| { let field = snake_ident(&p.name); let ty = type_tokens(&ir.records, p.ty); quote! { pub #field: #ty, } }); let props_default = ir.props.iter().all(|p| p.default.is_some()).then(|| { let defaults = ir.props.iter().map(|p| { let field = snake_ident(&p.name); let value = literal_tokens(p.default.as_ref().expect("checked"), Some(p.ty)); quote! { #field: #value, } }); quote! { impl ::core::default::Default for #props_name { fn default() -> Self { Self { #(#defaults)* } } } } }); // --- Cx struct --- let cx_prop_fields = ir.props.iter().map(|p| { let field = snake_ident(&p.name); let ty = type_tokens(&ir.records, p.ty); quote! { pub #field: ::guiduck::core::signals::ReadSignal<#ty>, } }); let state_fields = ir.states.iter().map(|s| { let field = snake_ident(&s.name); let ty = type_tokens(&ir.records, s.ty); quote! { pub #field: ::guiduck::core::signals::Signal<#ty>, } }); let output_fields = ir.outputs.iter().map(|o| { let field = snake_ident(&o.name); let ty = scalar_type_tokens(o.ty); quote! { pub #field: ::guiduck::core::component::Emitter<#ty>, } }); // The read-only counterpart, generated only when a query handler needs it: // props and outputs as in `Cx`, but state as `ReadSignal` — readable, not // settable. let query_cx_struct = has_queries.then(|| { let props = ir.props.iter().map(|p| { let field = snake_ident(&p.name); let ty = type_tokens(&ir.records, p.ty); quote! { pub #field: ::guiduck::core::signals::ReadSignal<#ty>, } }); let states = ir.states.iter().map(|s| { let field = snake_ident(&s.name); let ty = type_tokens(&ir.records, s.ty); quote! { pub #field: ::guiduck::core::signals::ReadSignal<#ty>, } }); let outputs = ir.outputs.iter().map(|o| { let field = snake_ident(&o.name); let ty = scalar_type_tokens(o.ty); quote! { pub #field: ::guiduck::core::component::Emitter<#ty>, } }); quote! { /// The read-only context a query handler receives: it may read /// state but not `set` it — a query answers, it does not act. pub struct #query_cx_name { #(#props)* #(#states)* #(#outputs)* } } }); // --- Logic trait --- let logic_methods = ir.handlers.iter().map(|h| { let method = snake_ident(&h.name); let params = h.payloads.iter().enumerate().map(|(i, ty)| { let name = format_ident!("value{i}"); match ty { TypeKind::String => quote! { #name: &str, }, ty => { let ty = scalar_type_tokens(*ty); quote! { #name: #ty, } } } }); // A handler wired to a widget query returns that query's value and gets // the read-only context; every other handler returns nothing and gets // the full one. let (cx_ty, returns) = match &h.returns { Some(path) => { let ty = type_path_tokens(path); (&query_cx_name, quote! { -> #ty }) } None => (&cx_name, quote! {}), }; quote! { fn #method(&mut self, cx: #cx_ty, #(#params)*) #returns; } }); // One logic-factory method per instantiated component type: the parent's // logic supplies each child instance's logic, so a missing factory is a // compile error at the impl site, like a missing handler. let logic_factories = instantiated_components(ir).into_iter().map(|child| { let method = format_ident!("{}", component_method_name(child)); let child_logic = format_ident!("{}Logic", child); quote! { /// Logic for one instance of the named child component. fn #method(&mut self) -> impl #child_logic; } }); // A component with no handlers and no child factories has an empty // Logic trait; `()` implements it, so pure-presentation components // mount without a logic type. let unit_logic = (ir.handlers.is_empty() && instantiated_components(ir).is_empty()).then(|| { quote! { impl #logic_name for () {} } }); // --- mount body --- let prop_signal_inits = ir.props.iter().map(|p| { let var = snake_ident(&p.name); let ty = type_tokens(&ir.records, p.ty); quote! { let #var: ::guiduck::core::signals::Signal<#ty> = ::guiduck::core::signals::Signal::new(props.#var); } }); let state_inits = ir.states.iter().map(|s| { let var = snake_ident(&s.name); let ty = type_tokens(&ir.records, s.ty); let init = literal_tokens(&s.init, Some(s.ty)); quote! { let #var: ::guiduck::core::signals::Signal<#ty> = ::guiduck::core::signals::Signal::new(#init); } }); let emitter_inits = ir.outputs.iter().map(|o| { let var = snake_ident(&o.name); let ty = scalar_type_tokens(o.ty); quote! { let #var: ::guiduck::core::component::Emitter<#ty> = ::guiduck::core::component::Emitter::new(); } }); let cx_construction = cx_construction(ir, &cx_name); let nodes = build_nodes(&Unit::new(compiled, assets), &cx_construction); let prop_struct_fields = ir.props.iter().map(|p| { let field = snake_ident(&p.name); let ty = type_tokens(&ir.records, p.ty); quote! { /// A prop, signal-backed: the embedding side (a parent /// component or Rust code) drives it post-mount. pub #field: ::guiduck::core::signals::Signal<#ty>, } }); let state_struct_fields = ir.states.iter().map(|s| { let field = snake_ident(&s.name); let ty = type_tokens(&ir.records, s.ty); quote! { pub #field: ::guiduck::core::signals::Signal<#ty>, } }); let emitter_struct_fields = ir.outputs.iter().map(|o| { let field = snake_ident(&o.name); let ty = scalar_type_tokens(o.ty); quote! { pub #field: ::guiduck::core::component::Emitter<#ty>, } }); let prop_names = ir.props.iter().map(|p| snake_ident(&p.name)); let state_names = ir.states.iter().map(|s| snake_ident(&s.name)); let emitter_names = ir.outputs.iter().map(|o| snake_ident(&o.name)); let mount_body = quote! { let logic = ::std::rc::Rc::new(::std::cell::RefCell::new(logic)); #(#prop_signal_inits)* #(#state_inits)* #(#emitter_inits)* #nodes { // The hook runs with everything in place — bindings attached, so a // signal it sets is already being watched. Nothing is dispatching, // so there is no propagation to stop and the consume flag is dead. let __consume = ::std::rc::Rc::new(::std::cell::Cell::new(false)); logic.borrow_mut().mounted(#cx_construction); } Self { root: __node0, #(#prop_names,)* #(#state_names,)* #(#emitter_names,)* } }; let mount_fns = if has_slot(ir) { quote! { /// Build the component's widget subtree under `parent` from the /// version compiled into this binary, with an empty slot; see /// [`Self::mount_compiled_with_slot`] to project content. /// /// [`Self::mount`] is what an application calls; this is the /// specific path, for a caller that wants the typed handle back /// or wants the choice pinned (every test does). pub fn mount_compiled( tree: &mut ::guiduck::core::WidgetTree, parent: ::core::option::Option<::guiduck::core::WidgetId>, props: #props_name, logic: impl #logic_name, ) -> Self { Self::mount_compiled_with_slot(tree, parent, props, logic, |_, _| {}) } /// Build the component's widget subtree under `parent`, wiring /// bindings and events; `__slot` is invoked at the `(slot)` /// position to build the projected content. pub fn mount_compiled_with_slot( tree: &mut ::guiduck::core::WidgetTree, parent: ::core::option::Option<::guiduck::core::WidgetId>, props: #props_name, logic: impl #logic_name, __slot: impl FnOnce( &mut ::guiduck::core::WidgetTree, ::core::option::Option<::guiduck::core::WidgetId>, ), ) -> Self { #mount_body } } } else { quote! { /// Build the component's widget subtree under `parent`, wiring /// bindings and events, from the version compiled into this /// binary. /// /// [`Self::mount`] is what an application calls; this is the /// specific path, for a caller that wants the typed handle back /// or wants the choice pinned (every test does). pub fn mount_compiled( tree: &mut ::guiduck::core::WidgetTree, parent: ::core::option::Option<::guiduck::core::WidgetId>, props: #props_name, logic: impl #logic_name, ) -> Self { #mount_body } } }; // The outer entry point. The two specific ones — `mount` and // `DevMount::new` — stay exactly as they are and remain the way tests // address a path deliberately; this only chooses between them. // // Live structure when *this* build is a development build and the source // file is still where the build found it. Both halves are load-bearing: // the profile is what says "I am building this app" rather than "I am // testing what I will ship", and the file check is what makes a stray // debug binary harmless on someone else's machine, where the path does not // exist. `GUIDUCK_LIVE=1`/`0` overrides either way, for iterating in // release or shipping a debug build deliberately. let mount_wrapper = quote! { /// Mount this component the way this build wants it: from the live /// `.gdc` while you are developing, from the compiled version in /// anything you ship. /// /// The entry point an application's `main` wants — no branch, no /// path, no configuration. It returns nothing, because the live path /// has no typed handle to give: its state lives in the interpreter /// and is rebuilt on every reload. Call [`Self::mount_compiled`] when /// you want the handle, or want the choice pinned rather than made /// for you — which is what a test wants, since calling this one in a /// development build would quietly test the interpreted path. /// /// # Panics /// /// If the live file is present but does not compile. That is a /// development-time state with a developer looking at it, and the /// diagnostics are the useful thing to say. pub fn mount( tree: &mut ::guiduck::core::WidgetTree, parent: ::core::option::Option<::guiduck::core::WidgetId>, props: #props_name, logic: impl #logic_name, ) { const __SOURCE: &::core::primitive::str = #source_path; // Short-circuits when the interpreter is not in this build: // no environment read, no filesystem probe, and the dev branch // below folds away entirely. let __live = ::guiduck::rt::AVAILABLE && match ::std::env::var("GUIDUCK_LIVE") { ::core::result::Result::Ok(v) => v != "0", ::core::result::Result::Err(_) => { ::core::cfg!(debug_assertions) && ::std::path::Path::new(__SOURCE).is_file() } }; if __live && ::std::path::Path::new(__SOURCE).is_file() { match ::guiduck::rt::DevMount::new( tree, parent, __SOURCE, Self::dev_spec(props, logic), ) { ::core::result::Result::Ok(dev) => { tree.set_live_reload(dev); return; } // The file is there and broken: say so loudly rather than // quietly running the last build's UI while the developer // wonders why their edit did nothing. ::core::result::Result::Err(e) => { ::std::panic!("{}", e); } } } if __live { ::std::eprintln!( "guiduck: no component source at {__SOURCE}; \ using the compiled component" ); } Self::mount_compiled(tree, parent, props, logic); } }; let dyn_registry = dyn_registry( ir, &name, &props_name, &cx_name, &logic_name, &dyn_name, host, ); quote! { #(#tracking)* #(#record_structs)* pub struct #props_name { #(#prop_fields)* } #props_default /// Clone to keep a handler's context past the handler. /// /// Work that finishes later — a file chooser, a download, a worker /// thread — needs somewhere to put its answer, and that somewhere is /// the same state a handler writes. Signals are `Copy` handles and /// emitters are owned, so keeping the whole context costs nothing and /// saves capturing them one at a time. [`Self::consume`] on a kept /// context is a no-op, since there is no dispatch left to stop. #[derive(Clone)] pub struct #cx_name { #(#cx_prop_fields)* #(#state_fields)* #(#output_fields)* __consume: ::std::rc::Rc<::std::cell::Cell>, } impl #cx_name { /// Signal that this handler consumed the event: no handler later in /// the dispatch — a deeper capture, or a bubbling ancestor — sees /// it. A no-op off the event path (an instance output, a query), /// where propagation is not a thing to stop. pub fn consume(&self) { self.__consume.set(true); } } #query_cx_struct pub trait #logic_name: 'static { /// Run once, after the component's subtree is built and every /// binding and wire is attached. /// /// This is where state that comes from outside the component /// language is seeded — a document read from disk, rows from a /// database — since a `:init` is a literal and a handler only runs /// in answer to an event. Setting a signal here drives the /// bindings that already depend on it, so the component's first /// frame shows the loaded data. /// /// The default does nothing. fn mounted(&mut self, cx: #cx_name) { let _ = cx; } #(#logic_methods)* #(#logic_factories)* } #unit_logic pub struct #name { /// The subtree's root widget. pub root: ::guiduck::core::WidgetId, #(#prop_struct_fields)* #(#state_struct_fields)* #(#emitter_struct_fields)* } impl #name { #mount_fns #mount_wrapper } #dyn_registry } } /// The dev-mode reassembly of the typed `Cx` from a `DynCx`, shared by every /// path that runs a compiled handler against interpreted state: an event /// through `invoke`, and a query resolver through `install_query`. /// /// The lookups read with `?`, so both callers wrap them in an /// `Option`-returning block; a record-list state becomes a temporary typed /// signal the caller must then dispose (a query) or write back and dispose (an /// event). struct DevCxReassembly { /// `let field = cx.getter(name)?;` for every prop, state, and output. lookups: TokenStream, /// The typed `Cx { … }` built from those locals. cx_value: TokenStream, /// Write each record-list temp back to its dynamic signal, then dispose it. writebacks: TokenStream, /// Dispose each record-list temp without writing back. disposals: TokenStream, } fn dev_cx_reassembly(ir: &Ir, cx_name: &proc_macro2::Ident) -> DevCxReassembly { let prop_lookups = ir.props.iter().map(|p| { let var = snake_ident(&p.name); let getter = dyn_prop_getter(p.ty); let prop_name = &p.name; quote! { let #var = cx.#getter(#prop_name)?.read_only(); } }); let state_lookups = ir.states.iter().map(|s| { let var = snake_ident(&s.name); let state_name = &s.name; match s.ty.kind { ElemKind::Record(index) => { let record_name = format_ident!("{}", ir.records[index as usize].name); let dyn_var = format_ident!("__dyn_{var}"); quote! { let #dyn_var = cx.signal_list_record(#state_name)?; let #var: ::guiduck::core::signals::Signal< ::std::vec::Vec<#record_name>, > = ::guiduck::core::signals::Signal::new( #dyn_var .get_untracked() .iter() .map(#record_name::__from_dyn) .collect(), ); } } ElemKind::Scalar(_) => { let getter = dyn_signal_getter(s.ty); quote! { let #var = cx.#getter(#state_name)?; } } } }); let emitter_lookups = ir.outputs.iter().map(|o| { let var = snake_ident(&o.name); let getter = dyn_emitter_getter(ValueTy::scalar(o.ty)); let output_name = &o.name; quote! { let #var = cx.#getter(#output_name)?; } }); let prop_fields = ir.props.iter().map(|p| snake_ident(&p.name)); let state_fields = ir.states.iter().map(|s| snake_ident(&s.name)); let output_fields = ir.outputs.iter().map(|o| snake_ident(&o.name)); let cx_value = quote! { #cx_name { #(#prop_fields,)* #(#state_fields,)* #(#output_fields,)* __consume: ::std::rc::Rc::clone(&__consume), } }; let writebacks = ir.states.iter().filter_map(|s| { let var = snake_ident(&s.name); match s.ty.kind { ElemKind::Record(_) => { let dyn_var = format_ident!("__dyn_{var}"); Some(quote! { #dyn_var.set( #var .get_untracked() .iter() .map(|__record| __record.__to_dyn()) .collect(), ); #var.dispose(); }) } ElemKind::Scalar(_) => None, } }); let disposals = ir.states.iter().filter_map(|s| { let var = snake_ident(&s.name); match s.ty.kind { ElemKind::Record(_) => Some(quote! { #var.dispose(); }), ElemKind::Scalar(_) => None, } }); DevCxReassembly { lookups: quote! { #(#prop_lookups)* #(#state_lookups)* #(#emitter_lookups)* }, cx_value, writebacks: quote! { #(#writebacks)* }, disposals: quote! { #(#disposals)* }, } } /// One `install_query` match arm per distinct query handler: downcast the /// widget to its concrete type and install a resolver that calls the compiled /// handler, reassembling the typed context from the interpreter's `DynCx` at /// resolve time. /// /// This is the dev-mode twin of [`query_wire_tokens`] — the compiled path /// installs the same resolver against typed signals; here it is installed /// against the interpreter's dynamic ones. Reassembly failure falls back to the /// return type's `Default`, which for a query is "no value" — exactly the /// alt-text fallback, and why the return type must be `Default`. fn query_install_arms(ir: &Ir, cx_name: &proc_macro2::Ident) -> Vec { let reassembly = dev_cx_reassembly(ir, cx_name); let query_cx_name = format_ident!("{}QueryCx", ir.name); let query_cx = query_cx_construction(ir, &query_cx_name); let mut seen: Vec = Vec::new(); let mut arms = Vec::new(); for node in &ir.nodes { let IrWidget::Widget(widget) = &node.widget else { continue; }; for query in &widget.queries { if seen.contains(&query.handler) { continue; } seen.push(query.handler.clone()); let handler_name = &query.handler; let method = snake_ident(&query.handler); let widget_ty = type_path_tokens(&widget.type_path); let setter = snake_ident(&query.setter); let ret = type_path_tokens(&query.returns); let arg_ty = match query.payload { TypeKind::String => quote! { &str }, other => scalar_type_tokens(other), }; let lookups = reassembly.lookups.clone(); let query_cx = query_cx.clone(); let disposals = reassembly.disposals.clone(); arms.push(quote! { #handler_name => { if let ::core::option::Option::Some(__w) = widget.downcast_mut::<#widget_ty>() { let logic = ::std::rc::Rc::clone(&self.logic); let cx = ::std::rc::Rc::clone(cx); __w.#setter(move |__src: #arg_ty| -> #ret { (|| -> ::core::option::Option<#ret> { #lookups let __typed_cx = #query_cx; let __result = logic.borrow_mut().#method(__typed_cx, __src); #disposals ::core::option::Option::Some(__result) })() .unwrap_or_default() }); } } }); } } arms } /// The dev-mode registry: a `DynHandlers` impl that reassembles the typed /// `Cx` from a `DynCx` per invocation, plus `dev_spec` bundling it with a /// dynamic snapshot of the props. fn dyn_registry( ir: &Ir, name: &proc_macro2::Ident, props_name: &proc_macro2::Ident, cx_name: &proc_macro2::Ident, logic_name: &proc_macro2::Ident, dyn_name: &proc_macro2::Ident, host: &Host<'_>, ) -> TokenStream { let Host { search_dirs, widget_dirs, asset_dirs, .. } = host; let component_str = ir.name.clone(); // Query handlers are installed as resolvers, not dispatched as events, and // their read-only context is a different type — so they take no `invoke` // arm. let arms = ir.handlers.iter().filter(|h| h.returns.is_none()).map(|h| { let handler_name = &h.name; let method = snake_ident(&h.name); let DevCxReassembly { lookups, cx_value, writebacks, .. } = dev_cx_reassembly(ir, cx_name); // Re-type the dynamic arguments for the logic method, positionally: // strings pass by reference, the numeric widths narrow from the // DynValue carriers. let count = h.payloads.len(); let extracts = h.payloads.iter().enumerate().map(|(i, ty)| { let name = format_ident!("value{i}"); let index = i; match ty { TypeKind::String => quote! { let ::guiduck::core::component::DynValue::Str(#name) = &args[#index] else { return ::core::option::Option::None; }; let #name: &str = #name; }, TypeKind::I32 => quote! { let ::guiduck::core::component::DynValue::I64(#name) = &args[#index] else { return ::core::option::Option::None; }; let #name = *#name as i32; }, TypeKind::I64 => quote! { let ::guiduck::core::component::DynValue::I64(#name) = &args[#index] else { return ::core::option::Option::None; }; let #name = *#name; }, TypeKind::F32 => quote! { let ::guiduck::core::component::DynValue::F64(#name) = &args[#index] else { return ::core::option::Option::None; }; let #name = *#name as f32; }, TypeKind::F64 => quote! { let ::guiduck::core::component::DynValue::F64(#name) = &args[#index] else { return ::core::option::Option::None; }; let #name = *#name; }, TypeKind::Bool => quote! { let ::guiduck::core::component::DynValue::Bool(#name) = &args[#index] else { return ::core::option::Option::None; }; let #name = *#name; }, } }); let arg_names = (0..count).map(|i| format_ident!("value{i}")); let call = quote! { if args.len() != #count { return ::core::option::Option::None; } #(#extracts)* self.logic.borrow_mut().#method(#cx_value, #(#arg_names),*); }; quote! { #handler_name => (|| -> ::core::option::Option { // A fresh consume flag the reassembled cx carries; the compiled // handler sets it through `cx.consume()`, and the interpreter // stops propagation on the result, exactly as codegen does. let __consume = ::std::rc::Rc::new(::std::cell::Cell::new(false)); #lookups #call #writebacks ::core::option::Option::Some(__consume.get()) })(), } }); let prop_snapshots = ir.props.iter().map(|p| { let field = snake_ident(&p.name); let prop_name = &p.name; // Props are boundary types: validation guarantees scalars here. let ElemKind::Scalar(prop_kind) = p.ty.kind else { unreachable!("record-typed props are rejected during validation"); }; let scalar = |value: TokenStream| match prop_kind { TypeKind::I32 | TypeKind::I64 => quote! { ::guiduck::core::component::DynValue::I64(#value as i64) }, TypeKind::F32 | TypeKind::F64 => quote! { ::guiduck::core::component::DynValue::F64(#value as f64) }, TypeKind::Bool => quote! { ::guiduck::core::component::DynValue::Bool(#value) }, TypeKind::String => quote! { ::guiduck::core::component::DynValue::Str(#value.clone()) }, }; let value = if p.ty.list { let element = scalar(quote! { (*__element) }); let element = match prop_kind { TypeKind::String => scalar(quote! { __element }), _ => element, }; quote! { ::guiduck::core::component::DynValue::List( props.#field.iter().map(|__element| #element).collect(), ) } } else { scalar(quote! { props.#field }) }; quote! { (::std::string::String::from(#prop_name), #value), } }); let child_factories = instantiated_components(ir).into_iter().map(|child| { let child_ty = format_ident!("{}", child); let factory = format_ident!("{}", component_method_name(child)); quote! { children.insert( ::std::string::String::from(#child), ::std::rc::Rc::new({ let logic = ::std::rc::Rc::clone(&logic); move || #child_ty::child_spec(logic.borrow_mut().#factory()) }), ); } }); let query_arms = query_install_arms(ir, cx_name); let mounted_arm = { let DevCxReassembly { lookups, cx_value, writebacks, .. } = dev_cx_reassembly(ir, cx_name); quote! { // The same reassembly `invoke` does: the hook is compiled logic // run against interpreted state, so what it sets must land back in // the interpreter's signals. Nothing is dispatching, so the consume // flag is dead. let _: ::core::option::Option<()> = (|| { let __consume = ::std::rc::Rc::new(::std::cell::Cell::new(false)); #lookups self.logic.borrow_mut().mounted(#cx_value); #writebacks ::core::option::Option::Some(()) })(); } }; quote! { /// Dev-mode handler registry for the runtime interpreter: handlers /// stay compiled Rust while structure and bindings are interpreted. /// The logic is shared with the child-spec factories, which mint a /// fresh child logic per nested instance. pub struct #dyn_name { logic: ::std::rc::Rc<::std::cell::RefCell>, } impl ::guiduck::core::component::DynHandlers for #dyn_name { fn invoke( &mut self, name: &str, cx: &::guiduck::core::component::DynCx, args: &[::guiduck::core::component::DynValue], ) -> bool { let handled: ::core::option::Option = match name { #(#arms)* _ => ::core::option::Option::None, }; match handled { ::core::option::Option::Some(consumed) => consumed, ::core::option::Option::None => { ::guiduck::core::component::warn_handler_skipped(#component_str, name); false } } } fn install_query( &self, name: &str, widget: &mut dyn ::guiduck::core::Widget, cx: &::std::rc::Rc<::guiduck::core::component::DynCx>, ) { match name { #(#query_arms)* _ => {} } } fn mounted(&mut self, cx: &::guiduck::core::component::DynCx) { #mounted_arm } } impl #name { /// The compiled side of a *nested* dev-mode instance: the /// handler registry plus a spec factory per component this one /// instantiates. Prop values come from the parent's file, so /// there is no snapshot here. pub fn child_spec( logic: impl #logic_name, ) -> ::guiduck::core::component::ComponentSpec { let logic = ::std::rc::Rc::new(::std::cell::RefCell::new(logic)); let mut children: ::std::collections::HashMap< ::std::string::String, ::std::rc::Rc ::guiduck::core::component::ComponentSpec>, > = ::std::collections::HashMap::new(); #(#child_factories)* ::guiduck::core::component::ComponentSpec { handlers: ::std::rc::Rc::new(::std::cell::RefCell::new(#dyn_name { logic, })), props: ::std::vec::Vec::new(), children, search_path: ::std::vec![ #(::std::path::PathBuf::from(#search_dirs),)* ], widget_path: ::std::vec![ #(::std::path::PathBuf::from(#widget_dirs),)* ], asset_path: ::std::vec![ #(::std::path::PathBuf::from(#asset_dirs),)* ], } } /// Bundle typed props and logic for a dev-mode (hot-reload) /// mount driven by the runtime interpreter. pub fn dev_spec( props: #props_name, logic: impl #logic_name, ) -> ::guiduck::core::component::ComponentSpec { let mut spec = Self::child_spec(logic); spec.props = ::std::vec![#(#prop_snapshots)*]; spec } } } } fn dyn_signal_getter(ty: ValueTy) -> proc_macro2::Ident { format_ident!("signal_{}", dyn_getter_suffix(ty)) } fn dyn_prop_getter(ty: ValueTy) -> proc_macro2::Ident { format_ident!("prop_{}", dyn_getter_suffix(ty)) } fn dyn_getter_suffix(ty: ValueTy) -> String { let scalar = match ty.kind { ElemKind::Scalar(TypeKind::I32) => "i32", ElemKind::Scalar(TypeKind::I64) => "i64", ElemKind::Scalar(TypeKind::F32) => "f32", ElemKind::Scalar(TypeKind::F64) => "f64", ElemKind::Scalar(TypeKind::Bool) => "bool", ElemKind::Scalar(TypeKind::String) => "string", ElemKind::Record(_) => "record", }; if ty.list { format!("list_{scalar}") } else { scalar.to_owned() } } fn dyn_emitter_getter(ty: ValueTy) -> proc_macro2::Ident { format_ident!("emitter_{}", dyn_getter_suffix(ty)) } /// Tokens constructing a `Cx` value inside an event closure, where prop and /// state signals and emitters are in scope by their snake names. fn cx_construction(ir: &Ir, cx_name: &proc_macro2::Ident) -> TokenStream { let props = ir.props.iter().map(|p| { let field = snake_ident(&p.name); quote! { #field: #field.read_only(), } }); let states = ir.states.iter().map(|s| { let field = snake_ident(&s.name); quote! { #field, } }); let outputs = ir.outputs.iter().map(|o| { let field = snake_ident(&o.name); quote! { #field: #field.clone(), } }); quote! { #cx_name { #(#props)* #(#states)* #(#outputs)* __consume: ::std::rc::Rc::clone(&__consume), } } } /// The read-only context a query handler receives — like [`cx_construction`] /// but every state signal is narrowed to a `ReadSignal`, so the handler cannot /// `set` it. Built from the same signal locals both the compiled mount and the /// dev-mode reassembly already have in scope. fn query_cx_construction(ir: &Ir, query_cx_name: &proc_macro2::Ident) -> TokenStream { let props = ir.props.iter().map(|p| { let field = snake_ident(&p.name); quote! { #field: #field.read_only(), } }); let states = ir.states.iter().map(|s| { let field = snake_ident(&s.name); quote! { #field: #field.read_only(), } }); let outputs = ir.outputs.iter().map(|o| { let field = snake_ident(&o.name); quote! { #field: #field.clone(), } }); quote! { #query_cx_name { #(#props)* #(#states)* #(#outputs)* } } } /// The component types a file instantiates, deduplicated, in node order. fn instantiated_components(ir: &Ir) -> Vec<&str> { let mut out: Vec<&str> = Vec::new(); for node in &ir.nodes { if let IrWidget::Component(name) = &node.widget && !out.contains(&name.as_str()) { out.push(name); } } out } /// Everything the node builders need about the compilation unit: the /// compiled IR, plus the absolute path the host resolved each `(asset "…")` /// reference to. Finding the files is the host's job — codegen only turns a /// resolution into an `include_bytes!` target — but the two travel together /// because a node's `:source` is meaningless without its resolution. struct Unit<'a> { compiled: &'a Compiled, assets: &'a BTreeMap, } impl<'a> Unit<'a> { fn new(compiled: &'a Compiled, assets: &'a BTreeMap) -> Self { Self { compiled, assets } } } /// Widget construction, bindings, and event wiring, from the root down. /// Node variables are `__node0` (root), `__node1`, …; recursion (rather /// than a flat loop) lets an instance's slot content be emitted inside the /// closure handed to the child's `mount_with_slot`. fn build_nodes(unit: &Unit<'_>, cx: &TokenStream) -> TokenStream { node_tokens(unit, 0, "e! { parent }, cx) } /// One node — and, below it, its subtree. fn node_tokens( unit: &Unit<'_>, index: usize, parent: &TokenStream, cx: &TokenStream, ) -> TokenStream { let compiled = unit.compiled; let ir = &compiled.root; let node = &ir.nodes[index]; let var = format_ident!("__node{index}"); // One widget node, whether the vocabulary it came from is the framework's // or an application's. Everything from the construction down — the layout // style, `:class`, `:enabled`, the pointer events, `:tooltip` — is the // vocabulary *every* widget has, so it is written once here and both fall // through it, exactly as both arms of `Registry::lookup` share the // universal tables. let widget = match &node.widget { IrWidget::Widget(widget) => widget, IrWidget::Component(child_name) => { return instance_tokens(unit, node, index, child_name, parent, cx); } // The slot marker: hand the projected content the enclosing // widget, mid-sequence, so sibling order is preserved. IrWidget::Slot => { return quote! { __slot(tree, #parent); }; } IrWidget::If => return if_tokens(unit, node, index, parent, cx), IrWidget::For => return for_tokens(unit, node, index, parent, cx), }; let constructor = construct_tokens(unit, widget); let style = style_tokens(&node.style); let mut out = quote! { let #var = tree.insert(#constructor, #style, #parent); }; if !node.classes.is_empty() { let classes = node.classes.iter().map(|c| { quote! { ::std::string::String::from(#c) } }); out.extend(quote! { tree.set_classes(#var, [#(#classes),*]); }); } out.extend(enabled_tokens(ir, node, &var)); for prop in &widget.props { out.extend(binding_tokens(ir, widget, prop, &var)); } for wire in &node.events { let IrEvent { event, handler, args, } = wire; let kind = event_kind_tokens(*event); let method = snake_ident(handler); let cx = cx.clone(); let emitter_clones = ir.outputs.iter().map(|o| { let name = snake_ident(&o.name); quote! { let #name = #name.clone(); } }); // Payload-carrying events pass the widget's value through to the // logic method; invocation wires evaluate their arguments at // dispatch time, in this node's scope (loop variables included). // Validation guaranteed the declaration matches either way. // The wire is keyed on the *payload kind* the registry declares, not // on which event it is: an event that delivers a `String` is plumbed // like every other one that does, so `:on-link` needed no arm here and // neither will the next one. let invoke = match event.payload() { Some(PayloadKind::String) if args.is_empty() => quote! { let ::core::option::Option::Some(value) = _ev.string_payload() else { return; }; logic.borrow_mut().#method(#cx, value); }, Some(PayloadKind::String) => { let call = invocation_call_tokens(ir, handler, args, &cx); quote! { let ::core::option::Option::Some(value) = _ev.string_payload() else { return; }; let __payload: ::std::string::String = value.to_owned(); #call } } Some(PayloadKind::Bool) if args.is_empty() => quote! { let ::core::option::Option::Some(value) = _ev.bool_payload() else { return; }; logic.borrow_mut().#method(#cx, value); }, Some(PayloadKind::Bool) => { let call = invocation_call_tokens(ir, handler, args, &cx); quote! { let ::core::option::Option::Some(value) = _ev.bool_payload() else { return; }; let __payload: bool = value; #call } } Some(PayloadKind::Number) if args.is_empty() => quote! { let ::core::option::Option::Some(value) = _ev.number_payload() else { return; }; logic.borrow_mut().#method(#cx, value); }, Some(PayloadKind::Number) => { let call = invocation_call_tokens(ir, handler, args, &cx); quote! { let ::core::option::Option::Some(value) = _ev.number_payload() else { return; }; let __payload: f64 = value; #call } } // Payload-free events — the pointer family, `:on-select`, // `:on-close` — are just an invocation. None => invocation_call_tokens(ir, handler, args, &cx), }; out.extend(quote! { { let logic = ::std::rc::Rc::clone(&logic); #(#emitter_clones)* tree.on_event(#var, #kind, move |ctx, _ev| { // A fresh consume flag per dispatch; `cx.consume()` sets it, // and a set flag stops propagation to the rest of the path. let __consume = ::std::rc::Rc::new(::std::cell::Cell::new(false)); #invoke if __consume.get() { ctx.stop_propagation(); } }); } }); } for wire in &widget.events { out.extend(user_event_tokens(ir, wire, &var, cx)); } for query in &widget.queries { out.extend(query_wire_tokens(ir, query, &widget.type_path, &var)); } // Asking for focus comes after the wires are attached and after // `:enabled`: a disabled widget cannot take focus, and a widget that takes // it at mount must be able to *report* that through its own // `:on-focus-change` — which it cannot if the wire is not there yet. if node.autofocus { out.extend(quote! { tree.set_focus(::core::option::Option::Some(#var)); }); } out.extend(focused_tokens(ir, node, &var)); if let Some(tooltip) = &node.tooltip { out.extend(quote! { tree.set_tooltip(#var, ::core::option::Option::Some(#tooltip.to_owned())); }); } // How a node's children mount. Content that projects a `(slot)` builds // inline: the slot's fill is the transient `__slot` closure, in lexical // scope only while the mount runs, so it cannot be captured into a builder // — and a widget projecting one therefore never defers, which is why this // is a static decision rather than a runtime one. // // Everything else is handed to the widget as a `ContentBuilder` through // `realize_content`; the tree consults the widget at runtime and either // builds it inline (the common case) or, for a popup, stores it to build on // open. So a menu, a dropdown, a dialog, and a plain container all mount // their children through one line — deferral is the widget's answer, not a // codegen branch, so an application widget that hosts a popup needs none. if subtree_has_slot(ir, index) { let child_parent = quote! { ::core::option::Option::Some(#var) }; for child in &node.children { out.extend(node_tokens(unit, *child, &child_parent, cx)); } } else if !node.children.is_empty() { let builder = content_builder_tokens(unit, node, cx); out.extend(quote! { tree.realize_content(#var, #builder); }); } // Accelerators are the exception: they must fire while a popup is closed // and its rows do not exist, so they register at mount, walking the // deferred subtree. Only a widget that defers its content leaves those rows // unbuilt, so the registration is gated on that runtime fact; a widget that // mounted the same subtree inline registers nothing extra (the block is // empty unless the subtree holds an accelerator, which only a menu's does). let accel = accel_tokens(unit, node, cx, &var); if !accel.is_empty() { out.extend(quote! { if tree.defers_content(#var) { #accel } }); } out } /// The `let __argN = …; logic.borrow_mut().method(cx, …)` sequence for an /// invocation wire, with string payloads passed by reference per the trait /// signature. fn invocation_call_tokens(ir: &Ir, handler: &str, args: &[Expr], cx: &TokenStream) -> TokenStream { let method = snake_ident(handler); let payloads = ir .handlers .iter() .find(|h| h.name == *handler) .map(|h| h.payloads.as_slice()) .unwrap_or(&[]); let arg_values = args.iter().enumerate().map(|(i, arg)| { let name = format_ident!("__arg{i}"); let value = expr_tokens(ir, arg, false); quote! { let #name = #value; } }); let args_tokens = payloads.iter().enumerate().map(|(i, ty)| { let name = format_ident!("__arg{i}"); match ty { TypeKind::String => quote! { &#name }, _ => quote! { #name }, } }); quote! { #(#arg_values)* logic.borrow_mut().#method(#cx, #(#args_tokens),*); } } /// Clone tokens for the captures a dynamic-region closure needs beyond the /// Copy signal handles: the logic Rc and every output emitter. fn dynamic_captures(ir: &Ir) -> TokenStream { let emitters = ir.outputs.iter().map(|o| { let name = snake_ident(&o.name); quote! { let #name = #name.clone(); } }); quote! { let logic = ::std::rc::Rc::clone(&logic); #(#emitters)* } } /// A branch/row subtree builder body: mount the node (appending), then move /// it to the region's position and yield its extent. /// /// The extent is a single widget for an ordinary body and the inner region's /// anchored run when the body is itself an `(if …)` or `(for …)` — which is /// the whole of what makes structural forms nest, since a region occupies a /// run of sibling slots rather than one. fn dynamic_body(unit: &Unit<'_>, child_index: usize, cx: &TokenStream) -> TokenStream { let body = node_tokens(unit, child_index, "e! { __dyn_parent }, cx); let extent = match unit.compiled.root.nodes[child_index].widget { IrWidget::If | IrWidget::For => { let region = format_ident!("__region{child_index}"); quote! { #region.borrow().extent() } } _ => { let var = format_ident!("__node{child_index}"); quote! { ::guiduck::core::widget::dynamic::Extent::Single(#var) } } }; quote! { #body let __extent = #extent; ::guiduck::core::widget::dynamic::place(tree, __dyn_parent, __extent, __at); } } /// A structural `(if …)`: a [`Conditional`] region driven by an effect. /// /// [`Conditional`]: guiduck::core::widget::dynamic::Conditional fn if_tokens( unit: &Unit<'_>, node: &IrNode, index: usize, parent: &TokenStream, cx: &TokenStream, ) -> TokenStream { let compiled = unit.compiled; let ir = &compiled.root; let Some(ControlIr::If { cond }) = &node.control else { unreachable!("If nodes carry If control data"); }; let region = format_ident!("__region{index}"); let cond = expr_tokens(ir, cond, false); let captures = dynamic_captures(ir); let then_body = dynamic_body(unit, node.children[0], cx); let build = match node.children.get(1) { Some(else_index) => { let else_body = dynamic_body(unit, *else_index, cx); quote! { if __cond { #then_body ::core::option::Option::Some(__extent) } else { #else_body ::core::option::Option::Some(__extent) } } } None => quote! { if __cond { #then_body ::core::option::Option::Some(__extent) } else { ::core::option::Option::None } }, }; quote! { let #region = ::std::rc::Rc::new(::std::cell::RefCell::new( ::guiduck::core::widget::dynamic::Conditional::new(tree, #parent), )); { let __region = ::std::rc::Rc::clone(&#region); let __commands = tree.commands(); #captures ::guiduck::core::signals::Effect::new(move || { let __cond: bool = #cond; let __region = ::std::rc::Rc::clone(&__region); #captures __commands.push(move |tree| { __region.borrow_mut().set( tree, __cond, move |tree, __dyn_parent, __at| #build, ); }); }); } } } /// A keyed `(for …)`: a [`KeyedList`] region reconciled by an effect. /// /// [`KeyedList`]: guiduck::core::widget::dynamic::KeyedList fn for_tokens( unit: &Unit<'_>, node: &IrNode, index: usize, parent: &TokenStream, cx: &TokenStream, ) -> TokenStream { let compiled = unit.compiled; let ir = &compiled.root; let Some(ControlIr::For { var, index: index_name, list, element, key, }) = &node.control else { unreachable!("For nodes carry For control data"); }; let region = format_ident!("__region{index}"); let elem_ty = elem_type_tokens(&ir.records, *element); let list_ident = snake_ident(list); let var_ident = snake_ident(var); let index_binding = index_name.as_ref().map(|name| { let ident = snake_ident(name); quote! { let #ident = __row_index; } }); // The key derives from a plain item value (rows do not exist yet when // keys are computed), so the loop names bind as `PlainValue`s — same // `.get()` spelling the expression emitter uses for signals. let key_of = match key { Some(key) => { let key_index_binding = index_name.as_ref().map(|name| { let ident = snake_ident(name); quote! { let #ident = ::guiduck::core::widget::dynamic::PlainValue( __key_index as i64, ); } }); let key_value = expr_tokens(ir, key, false); quote! { |__key_index: usize, __key_item: &#elem_ty| { let #var_ident = ::guiduck::core::widget::dynamic::PlainValue(__key_item.clone()); #key_index_binding #key_value } } } // Unkeyed: rows match by position. None => quote! { |__key_index: usize, _: &#elem_ty| __key_index as i64 }, }; let captures = dynamic_captures(ir); let row_body = dynamic_body(unit, node.children[0], cx); quote! { let #region = ::std::rc::Rc::new(::std::cell::RefCell::new( ::guiduck::core::widget::dynamic::KeyedList::new(tree, #parent), )); { let __region = ::std::rc::Rc::clone(&#region); let __commands = tree.commands(); #captures ::guiduck::core::signals::Effect::new(move || { let __items: ::std::vec::Vec<#elem_ty> = #list_ident.get(); // Signal updates for surviving rows happen here, in the // effect phase, so dependent effects re-run this flush. __region.borrow_mut().sync_rows(&__items, #key_of); let __region = ::std::rc::Rc::clone(&__region); #captures __commands.push(move |tree| { __region.borrow_mut().reconcile( tree, &__items, #key_of, |tree, __dyn_parent, __at, __item: ::guiduck::core::signals::Signal<#elem_ty>, __row_index: ::guiduck::core::signals::Signal| { let #var_ident = __item; #index_binding #row_body __extent }, ); }); }); } } } /// One component instance: mount the child with every prop populated /// (given, or the child's declared default), install one driving effect per /// non-literal prop expression, and subscribe output wires to the parent's /// logic methods. fn instance_tokens( unit: &Unit<'_>, node: &IrNode, index: usize, child_name: &str, parent: &TokenStream, cx: &TokenStream, ) -> TokenStream { let compiled = unit.compiled; let ir = &compiled.root; let child = compiled .components .get(child_name) .expect("component references resolve during compilation"); let child_ty = format_ident!("{}", child_name); let child_props_ty = format_ident!("{}Props", child_name); let factory = format_ident!("{}", component_method_name(child_name)); let inst = format_ident!("__component{index}"); let var = format_ident!("__node{index}"); let prop_inits = child.props.iter().map(|declared| { let field = snake_ident(&declared.name); let given = node .component_props .iter() .find(|(name, _)| *name == declared.name); let value = match given { // A literal coerces to the child's declared type; a reactive // expression's initial value is its evaluation right now (the // driving effect below keeps it current). Some((_, expr)) => match literal_of(expr) { Some(literal) => literal_tokens(&literal, Some(declared.ty)), None => expr_tokens(ir, expr, false), }, None => literal_tokens( declared .default .as_ref() .expect("validation checked required props"), Some(declared.ty), ), }; quote! { #field: #value, } }); // The child logic is minted *before* the mount call: as a call // argument, the `borrow_mut` temporary would live for the whole // statement — including a slot closure that borrows `logic` again. let child_logic = format_ident!("__logic{index}"); let mint_logic = quote! { let #child_logic = logic.borrow_mut().#factory(); }; // Slot content — this instance's children — is parent content built // inside a closure the child invokes at its `(slot)`: names were // resolved in this file's scope, and the captured `logic`/emitters are // this component's. let mount_call = if node.children.is_empty() { quote! { #child_ty::mount_compiled( tree, #parent, #child_props_ty { #(#prop_inits)* }, #child_logic, ) } } else { let slot_parent = quote! { __slot_parent }; let content = node .children .iter() .map(|child| node_tokens(unit, *child, &slot_parent, cx)); let emitter_clones = ir.outputs.iter().map(|o| { let name = snake_ident(&o.name); quote! { let #name = #name.clone(); } }); quote! { #child_ty::mount_compiled_with_slot( tree, #parent, #child_props_ty { #(#prop_inits)* }, #child_logic, { let logic = ::std::rc::Rc::clone(&logic); #(#emitter_clones)* move |tree: &mut ::guiduck::core::WidgetTree, __slot_parent: ::core::option::Option<::guiduck::core::WidgetId>| { #(#content)* } }, ) } }; let mut out = quote! { #mint_logic let #inst = #mount_call; let #var = #inst.root; }; // Instance-level `:class` merges into the root widget's own classes, // and the flex-item layout overrides patch its style in place. if !node.classes.is_empty() { let classes = node.classes.iter().map(|c| { quote! { ::std::string::String::from(#c) } }); out.extend(quote! { tree.add_classes(#var, [#(#classes),*]); }); } let patch = style_patch_tokens(&node.style); if !patch.is_empty() { out.extend(quote! { tree.update_style(#var, |__style| { #patch }); }); } out.extend(enabled_tokens(ir, node, &var)); for (prop_name, expr) in &node.component_props { if literal_of(expr).is_some() { continue; } let field = snake_ident(prop_name); let value = expr_tokens(ir, expr, false); out.extend(quote! { { let __target = #inst.#field; ::guiduck::core::signals::Effect::new(move || __target.set(#value)); } }); } for wire in &node.component_outputs { let output_field = snake_ident(&wire.output); let method = snake_ident(&wire.handler); let output_ty = child .outputs .iter() .find(|o| o.name == wire.output) .expect("validation checked output wires") .ty; let emitter_clones = ir.outputs.iter().map(|o| { let name = snake_ident(&o.name); quote! { let #name = #name.clone(); } }); let cx = cx.clone(); let invoke = if wire.args.is_empty() { // Bare-name form: the output's value is the one argument. // `&String` coerces to the handler's `&str`; everything else // in the type set is `Copy` and passes by value. let pass = match output_ty { TypeKind::String => quote! { __value }, _ => quote! { *__value }, }; quote! { logic.borrow_mut().#method(#cx, #pass); } } else { // Invocation form: `payload` binds the emitted value; the // arguments evaluate at dispatch, in this node's scope. let bind_payload = match output_ty { TypeKind::String => quote! { let __payload = __value.clone(); }, _ => quote! { let __payload = *__value; }, }; let call = invocation_call_tokens(ir, &wire.handler, &wire.args, &cx); quote! { #bind_payload #call } }; out.extend(quote! { { let logic = ::std::rc::Rc::clone(&logic); #(#emitter_clones)* #inst.#output_field.subscribe(move |__value| { // An instance output has no dispatch to stop; `cx.consume()` // in a handler wired here is a no-op against a dead flag. let __consume = ::std::rc::Rc::new(::std::cell::Cell::new(false)); #invoke }); } }); } out } /// The literal an expression is, if it is one (the static/reactive split /// for component props). fn literal_of(expr: &Expr) -> Option { match expr { Expr::Int(v, _) => Some(Literal::Int(*v)), Expr::Float(v, _) => Some(Literal::Float(*v)), Expr::Bool(v, _) => Some(Literal::Bool(*v)), Expr::Str(template, _) if template.refs().next().is_none() => Some(Literal::Str( template .segments .iter() .filter_map(|s| match s { Segment::Literal(l) => Some(l.as_str()), Segment::Ref(..) => None, }) .collect(), )), _ => None, } } /// Build a widget: `::default()`, then a setter call per /// property whose value is known now. /// /// **This is the only place a widget is constructed.** A `container` and an /// application's `markdown` come through it identically, because the contract /// is identical — `Default + Widget` plus a setter per property — and what a /// builtin's descriptor says is what a `.gdw` manifest says. There is no /// constructor-argument form to mirror: a property is a setter call, so a /// static value and a bound one reach the widget through the same method, and /// the only difference is when. fn construct_tokens(unit: &Unit<'_>, widget: &IrWidgetNode) -> TokenStream { let ty = type_path_tokens(&widget.type_path); let setters = widget.props.iter().map(|prop| { let method = snake_ident(&prop.setter); let value = match &prop.value { IrPropValue::Static(literal) => prop_literal_tokens(unit, literal, prop.ty), // A binding is applied by an effect, not at construction. IrPropValue::Binding(_) => return TokenStream::new(), }; quote! { __widget.#method(#value); } }); quote! { { let mut __widget = <#ty as ::core::default::Default>::default(); #(#setters)* __widget } } } /// One reactive binding: an effect computing the value, applied through the /// property's setter (which classifies the dirt). /// /// The twin of the setter calls [`construct_tokens`] emits, and — the point — /// it reads the same three facts out of the same [`IrWidgetProp`]: the /// widget's type, the method, and the type that method takes. A builtin and a /// declared widget cannot disagree about a value's width, because nothing here /// knows which it is looking at. fn binding_tokens( ir: &Ir, widget: &IrWidgetNode, prop: &IrWidgetProp, var: &proc_macro2::Ident, ) -> TokenStream { let IrPropValue::Binding(expr) = &prop.value else { return TokenStream::new(); }; let ty = type_path_tokens(&widget.type_path); let value_ty = prop_type_tokens(prop.ty); let value = prop_value_tokens(ir, expr, prop.ty); let method = snake_ident(&prop.setter); quote! { tree.bind::<#ty, #value_ty>( #var, move || #value, |__widget, __value| __widget.#method(__value), ); } } /// An expression at the type a property's setter takes. /// /// The numeric families widen and narrow freely across a `.gdc` boundary — /// validation admits an `i64` state on an `f32` property — so the cast is /// emitted here rather than left for rustc to reject against generated code. /// Being one function is what makes that true of every property of every /// widget: there is no second lane left to forget it. fn prop_value_tokens(ir: &Ir, expr: &Expr, ty: PropTy) -> TokenStream { let value = expr_tokens(ir, expr, ty == PropTy::Brush); match ty { // Already the setter's type; `String` has no cast, and a `bool` // expression is a `bool`. PropTy::Scalar(TypeKind::String | TypeKind::Bool) => value, PropTy::Scalar(kind) => { let kind = scalar_type_tokens(kind); quote! { ((#value) as #kind) } } // A brush expression evaluates to a `Color` (validation admits color // literals and `if`s choosing between them); the setter takes the // wider `Brush`. PropTy::Brush => quote! { ::guiduck::paint::Brush::from(#value) }, // A bound paragraph is a string: rich text with no styled runs. PropTy::RichText => quote! { ::guiduck::core::RichText::from(#value) }, PropTy::Graphic | PropTy::ScrollAxes => { unreachable!("validation admits no binding on this property") } } } /// A static property value, at the type its setter takes. fn prop_literal_tokens(unit: &Unit<'_>, literal: &Literal, ty: PropTy) -> TokenStream { match ty { PropTy::Scalar(kind) => literal_tokens(literal, Some(ValueTy::scalar(kind))), PropTy::Brush => { let color = literal_tokens(literal, None); quote! { ::guiduck::paint::Brush::from(#color) } } PropTy::Graphic => graphic_tokens(unit, literal), PropTy::RichText => rich_text_tokens(literal), PropTy::ScrollAxes => { let Literal::ScrollAxes(axes) = literal else { unreachable!("validated axis literal"); }; let variant = match axes { ScrollAxesIr::Vertical => quote! { Vertical }, ScrollAxesIr::Horizontal => quote! { Horizontal }, ScrollAxesIr::Both => quote! { Both }, }; quote! { ::guiduck::core::ScrollAxes::#variant } } } } /// The Rust type a property's setter takes. fn prop_type_tokens(ty: PropTy) -> TokenStream { match ty { PropTy::Scalar(kind) => scalar_type_tokens(kind), PropTy::Brush => quote! { ::guiduck::paint::Brush }, PropTy::Graphic => quote! { ::guiduck::core::graphic::Graphic }, PropTy::RichText => quote! { ::guiduck::core::RichText }, PropTy::ScrollAxes => quote! { ::guiduck::core::ScrollAxes }, } } /// A graphic literal: `(path …)` geometry, or an `(asset "…")` file's bytes, /// embedded at build time. fn graphic_tokens(unit: &Unit<'_>, literal: &Literal) -> TokenStream { match literal { Literal::Path(cmds) => { let cmds = cmds.iter().map(path_cmd_tokens); quote! { ::guiduck::core::graphic::Graphic::from_path_cmds(&[#(#cmds),*]) } } Literal::Asset(raw) => { // Resolution happened in the macro entry point, so this path is // absolute and exists. let absolute = unit.assets.get(raw).map(String::as_str).unwrap_or_default(); quote! { ::guiduck::core::graphic::Graphic::Image( match ::guiduck::core::asset::embedded_image( #raw, ::core::include_bytes!(#absolute), ) { ::core::result::Result::Ok(image) => image, ::core::result::Result::Err(why) => ::core::panic!( "guiduck: asset `{}` is not an image this build \ can decode: {}", #raw, why, ), } ) } } _ => unreachable!("validated graphic literal"), } } /// One event wire on a `.gdw`-declared widget. /// /// Every declared event arrives as the single [`EventKind::User`] — the kind /// enum is the closed vocabulary's, and a `Copy` kind cannot carry a name — so /// the wire matches the name it was written for before it fires. Which handler /// call to make is [`invocation_call_tokens`]', exactly as for a builtin; /// only reading the payload out is different, because a declared payload's /// type is the manifest's rather than the registry's. /// /// [`EventKind::User`]: guiduck::core::EventKind::User fn user_event_tokens( ir: &Ir, wire: &IrUserEvent, var: &proc_macro2::Ident, cx: &TokenStream, ) -> TokenStream { let event_name = &wire.name; let method = snake_ident(&wire.handler); let emitter_clones = ir.outputs.iter().map(|o| { let name = snake_ident(&o.name); quote! { let #name = #name.clone(); } }); let cx = cx.clone(); let invoke = match wire.payload { // Payload-free: a bare handler name or an invocation wire, both of // which `invocation_call_tokens` already spells. None => invocation_call_tokens(ir, &wire.handler, &wire.args, &cx), // Bare-name form: validation checked the handler declares exactly this // payload, so it passes straight through (strings by reference, per // the trait signature). // A `String` payload extracts as `&str`, which is what the trait // signature already wants; every other type in the set is `Copy`. Some(ty) if wire.args.is_empty() => { let extract = payload_extract_tokens(ty); quote! { #extract logic.borrow_mut().#method(#cx, __value); } } // Invocation form: `payload` binds the delivered value and the // arguments evaluate at dispatch, in this node's scope. Some(ty) => { let extract = payload_extract_tokens(ty); let bind = match ty { TypeKind::String => { quote! { let __payload: ::std::string::String = __value.to_owned(); } } _ => { let rust_ty = scalar_type_tokens(ty); quote! { let __payload: #rust_ty = __value; } } }; let call = invocation_call_tokens(ir, &wire.handler, &wire.args, &cx); quote! { #extract #bind #call } } }; quote! { { let logic = ::std::rc::Rc::clone(&logic); #(#emitter_clones)* tree.on_event(#var, ::guiduck::core::EventKind::User, move |ctx, _ev| { let ::core::option::Option::Some((__event, __payload_value)) = _ev.user() else { return; }; if __event != #event_name { return; } // A fresh consume flag per dispatch, exactly as a builtin event // wire carries: `cx.consume()` stops propagation. let __consume = ::std::rc::Rc::new(::std::cell::Cell::new(false)); #invoke if __consume.get() { ctx.stop_propagation(); } }); } } } /// A `:get-image (load payload)` query wire: install the handler as a resolver /// closure on the widget, instead of registering it as an event. /// /// The widget calls it synchronously — `payload` is the value it wants resolved /// (a URL), and the handler returns the query's declared type — so this is the /// one wire that hands the widget an answer rather than reporting to the app. fn query_wire_tokens( ir: &Ir, query: &IrQuery, type_path: &str, var: &proc_macro2::Ident, ) -> TokenStream { let ty = type_path_tokens(type_path); let setter = snake_ident(&query.setter); let ret = type_path_tokens(&query.returns); let method = snake_ident(&query.handler); let query_cx_name = format_ident!("{}QueryCx", ir.name); let cx = query_cx_construction(ir, &query_cx_name); let emitter_clones = ir.outputs.iter().map(|o| { let name = snake_ident(&o.name); quote! { let #name = #name.clone(); } }); // The handler receives exactly the value to resolve — a `String` payload as // `&str` (what the trait signature wants), any other scalar by value — and // returns the query's type. This is the one wire whose call is an // expression, not a statement, because a query answers. let arg_ty = match query.payload { TypeKind::String => quote! { &str }, other => scalar_type_tokens(other), }; quote! { { let logic = ::std::rc::Rc::clone(&logic); #(#emitter_clones)* tree.widget_mut::<#ty>(#var) .expect("the widget the query is written on exists") .#setter(move |__src: #arg_ty| -> #ret { logic.borrow_mut().#method(#cx, __src) }); } } } /// Bind `__value` to a declared event's payload at the width the manifest /// declared, out of the `Option<&UserValue>` the event carries. A payload of /// the wrong family is a widget that broke its own contract; the wire declines /// to fire rather than guessing, which is the erased spelling of the type /// mismatch that makes a mismatched `Commands::mutate` a no-op. fn payload_extract_tokens(ty: TypeKind) -> TokenStream { let carrier = match ty { TypeKind::I32 | TypeKind::I64 => quote! { as_int }, TypeKind::F32 | TypeKind::F64 => quote! { as_float }, TypeKind::Bool => quote! { as_bool }, TypeKind::String => quote! { as_str }, }; let narrow = match ty { TypeKind::I64 | TypeKind::Bool | TypeKind::String | TypeKind::F64 => TokenStream::new(), _ => { let rust_ty = scalar_type_tokens(ty); quote! { let __value = __value as #rust_ty; } } }; quote! { let ::core::option::Option::Some(__value) = __payload_value.and_then(::guiduck::core::event::UserValue::#carrier) else { return; }; #narrow } } /// A manifest's `(type "…")`, verbatim, as tokens. /// /// The compiler never resolves it — a proc-macro cannot see another crate's /// items — so this only tokenizes; rustc reports a path that does not exist, /// or is not `Default + Widget`, against the construction above. fn type_path_tokens(path: &str) -> TokenStream { path.parse().unwrap_or_else(|_| { let message = format!("`(type \"{path}\")` is not a Rust path"); quote! { ::core::compile_error!(#message) } }) } /// A widget's children as **deferred content**: a `ContentBuilder` that /// builds them into a parent chosen when it is called, instead of statements /// that mount them now. /// /// This emits the subtree through `node_tokens` — the same emitter every /// other widget goes through — so codegen keeps one definition of "build this /// node". (Not `dynamic_body`, which additionally `move_child`s its result /// into a region's slot; menu content is appended in order and has no slot to /// land in.) The captures are re-emitted inside the closure — signals are /// `Copy`, logic is an `Rc` — which is what makes this an `Fn` a menu can /// call on every open rather than a `FnOnce` spent at mount. /// Whether the subtree rooted at `index` contains a `(slot)` marker. /// /// Such content must mount inline rather than through a [`ContentBuilder`]: a /// slot is filled by the transient `__slot` closure `mount_with_slot` holds, /// which lives only while the mount runs, so it cannot be captured into a /// builder a popup calls later — and a widget that projects one therefore never /// defers its content. The walk follows the node tree; a widget that does defer /// (a menu, a dialog) never contains a slot, so it is never forced inline by /// this. fn subtree_has_slot(ir: &Ir, index: usize) -> bool { let node = &ir.nodes[index]; matches!(node.widget, IrWidget::Slot) || node.children.iter().any(|&c| subtree_has_slot(ir, c)) } fn content_builder_tokens(unit: &Unit<'_>, node: &IrNode, cx: &TokenStream) -> TokenStream { let ir = &unit.compiled.root; let captures = dynamic_captures(ir); let parent = quote! { __content_parent }; let bodies = node .children .iter() .map(|child| node_tokens(unit, *child, &parent, cx)); quote! { ::guiduck::core::content::ContentBuilder::new({ #captures move |tree, __parent| { let #parent = ::core::option::Option::Some(__parent); #(#bodies)* } }) } } /// Accelerator registrations for every item in a menu's deferred content. /// /// These are emitted at *mount*, beside the menu, not inside its /// `ContentBuilder` — an accelerator's whole point is firing while the menu /// is closed, and a closed menu's rows do not exist. So an accelerator and /// its row's `:on-select` share the *handler call*, both built from /// `invocation_call_tokens`, rather than the accelerator dispatching at a /// widget that may not be there. /// /// Validation guarantees no `:accel` sits inside `if` or `for`, so every one /// found here is unconditional and its arguments cannot read a loop variable. fn accel_tokens( unit: &Unit<'_>, node: &IrNode, cx: &TokenStream, var: &proc_macro2::Ident, ) -> TokenStream { let ir = &unit.compiled.root; let mut out = TokenStream::new(); let mut stack: Vec = node.children.clone(); while let Some(index) = stack.pop() { let child = &ir.nodes[index]; stack.extend(child.children.iter().copied()); let Some(accel) = &child.accel else { continue }; let text = accel.display(); let Some(wire) = child .events .iter() .find(|event| event.event == EventProp::Select) else { // An accelerator with nothing wired to it would be a key that // silently does nothing; say so where the file can be seen. continue; }; let call = invocation_call_tokens(ir, &wire.handler, &wire.args, cx); let captures = dynamic_captures(ir); out.extend(quote! { { #captures tree.on_menu_accel(#var, #text, move || { #captures // An accelerator firing is not a dispatch to stop. let __consume = ::std::rc::Rc::new(::std::cell::Cell::new(false)); #call }); } }); } out } /// A paragraph's content: a plain string, or a `(rich …)` as the joined string /// plus a span per run that differs from it. /// /// The nesting is already gone — the compiler resolved it — so this is a flat /// walk that accumulates byte offsets. fn rich_text_tokens(literal: &Literal) -> TokenStream { let runs: &[RichRun] = match literal { Literal::Rich(runs) => runs, // Plain text is rich text with no runs. Literal::Str(text) => { return quote! { ::guiduck::core::RichText::new(#text) }; } _ => unreachable!("validated text literal"), }; let full: String = runs.iter().map(|run| run.text.as_str()).collect(); let mut spans = TokenStream::new(); let mut at = 0usize; for run in runs { let start = at; at += run.text.len(); let mut style = quote! { ::guiduck::core::TextSpan::new() }; if run.bold { style = quote! { #style.bold() }; } if run.italic { style = quote! { #style.italic() }; } if run.underline { style = quote! { #style.underline() }; } if run.strikethrough { style = quote! { #style.strikethrough() }; } if let Some((r, g, b, a)) = run.color { style = quote! { #style.color(::guiduck::paint::Color::from_rgba8(#r, #g, #b, #a)) }; } if let Some(size) = run.size { let size = size as f32; style = quote! { #style.font_size(#size) }; } if let Some(target) = &run.link { style = quote! { #style.link(#target) }; } // A run with nothing set is the paragraph's own style: no span. if run.bold || run.italic || run.underline || run.strikethrough || run.color.is_some() || run.size.is_some() || run.link.is_some() { let end = at; spans.extend(quote! { .span(#start..#end, #style) }); } } quote! { ::guiduck::core::RichText::new(#full) #spans } } /// One `(path …)` drawing command, as the runtime enum. fn path_cmd_tokens(cmd: &PathCmd) -> TokenStream { let variant = quote! { ::guiduck::core::graphic::PathCmd }; match *cmd { PathCmd::Move(x, y) => quote! { #variant::Move(#x, #y) }, PathCmd::Line(x, y) => quote! { #variant::Line(#x, #y) }, PathCmd::Quad(cx, cy, x, y) => quote! { #variant::Quad(#cx, #cy, #x, #y) }, PathCmd::Cubic(a, b, c, d, x, y) => quote! { #variant::Cubic(#a, #b, #c, #d, #x, #y) }, PathCmd::Close => quote! { #variant::Close }, } } /// `:enabled` — a literal sets the flag at mount; anything else drives it /// through an effect (a tree-level mutation, so it goes through the command /// queue rather than `bind`'s widget access). fn enabled_tokens(ir: &Ir, node: &IrNode, var: &proc_macro2::Ident) -> TokenStream { bool_prop_tokens(ir, node.enabled.as_ref(), var, quote! { set_enabled }) } /// `:focused` — the same shape as `:enabled`, and the same reason: focus lives /// on the tree, not on the widget, so it is a command rather than a `bind`. fn focused_tokens(ir: &Ir, node: &IrNode, var: &proc_macro2::Ident) -> TokenStream { bool_prop_tokens(ir, node.focused.as_ref(), var, quote! { set_focused }) } /// A universal boolean tree property: a literal applies at mount, anything /// else drives `tree.(id, value)` from an effect. The `bool` /// annotation makes rustc the type checker for the expression, as with every /// binding. fn bool_prop_tokens( ir: &Ir, expr: Option<&Expr>, var: &proc_macro2::Ident, method: TokenStream, ) -> TokenStream { match expr { None => TokenStream::new(), Some(Expr::Bool(value, _)) => quote! { tree.#method(#var, #value); }, Some(expr) => { let value = expr_tokens(ir, expr, false); quote! { { let __commands = tree.commands(); ::guiduck::core::signals::Effect::new(move || { let __value: bool = #value; let __commands = __commands.clone(); __commands.push(move |tree| tree.#method(#var, __value)); }); } } } } } /// Lower an expression to Rust tokens. State reads become `signal.get()`, /// prop reads become `props.field` — inside binding closures and Cx /// construction both are in scope under those names. fn expr_tokens(ir: &Ir, expr: &Expr, brush: bool) -> TokenStream { match expr { // Numeric literals stay unsuffixed so inference gives them the type // of whatever they combine with (a `.gdc` `1` is width-polymorphic, // like a Rust integer literal). Expr::Int(v, _) => { let v = proc_macro2::Literal::i64_unsuffixed(*v); quote! { #v } } Expr::Float(v, _) => { let v = proc_macro2::Literal::f64_unsuffixed(*v); quote! { #v } } Expr::Bool(v, _) => quote! { #v }, Expr::Str(template, _) => { if brush { // Validation guaranteed this is a color literal. let text: String = template .segments .iter() .filter_map(|s| match s { Segment::Literal(l) => Some(l.as_str()), Segment::Ref(..) => None, }) .collect(); let Some(Literal::Color(r, g, b, a)) = parse_color(&text) else { unreachable!("validated color literal"); }; return quote! { ::guiduck::paint::Color::from_rgba8(#r, #g, #b, #a) }; } if template.refs().next().is_none() { let text: String = template .segments .iter() .filter_map(|s| match s { Segment::Literal(l) => Some(l.as_str()), Segment::Ref(..) => None, }) .collect(); quote! { ::std::string::String::from(#text) } } else { // Interpolation → format!. let mut fmt = String::new(); let mut args = Vec::new(); for segment in &template.segments { match segment { Segment::Literal(l) => { fmt.push_str(&l.replace('{', "{{").replace('}', "}}")); } Segment::Ref(name, _) => { fmt.push_str("{}"); args.push(name_tokens(ir, name)); } } } quote! { ::std::format!(#fmt #(, #args)*) } } } // `event.x` and friends read the event the wire is firing for, out of // the `_ev` every wire closure already has. The fallback arms are // unreachable in practice — validation binds `event` only on wires // whose data is the matching variant — but a wire is an ordinary // closure, so there is a value to produce rather than a panic to // justify. Expr::Path(segments, _) if segments[0] == "event" => { let field = segments.get(1).map(String::as_str).unwrap_or_default(); match field { "key" => quote! { ::std::string::String::from(_ev.key().unwrap_or_default()) }, "offset-x" | "offset-y" => { let axis = if field == "offset-x" { quote! { __o.0 } } else { quote! { __o.1 } }; quote! { match _ev.scrolled() { ::core::option::Option::Some(__o) => #axis, ::core::option::Option::None => 0.0f64, } } } _ => { let (read, fallback) = match field { "x" => (quote! { __p.local.x }, quote! { 0.0f64 }), "y" => (quote! { __p.local.y }, quote! { 0.0f64 }), "click-count" => (quote! { (__p.click_count as i32) }, quote! { 0i32 }), _ => unreachable!("validation admits only `event`'s own fields"), }; quote! { match _ev.pointer() { ::core::option::Option::Some(__p) => #read, ::core::option::Option::None => #fallback, } } } } } Expr::Path(segments, _) => name_tokens(ir, &segments.join(".")), Expr::List(items, _) => { let items = items.iter().map(|item| expr_tokens(ir, item, false)); quote! { ::std::vec![#(#items),*] } } // Validation confines calls to event wires, which never reach the // expression emitter. Expr::Call(..) => unreachable!("handler invocation in expression position"), Expr::RecordLit(name, fields, _) => { let name = format_ident!("{name}"); let fields = fields.iter().map(|(field, value)| { let field = snake_ident(field); let value = expr_tokens(ir, value, false); quote! { #field: #value, } }); quote! { #name { #(#fields)* } } } // Validation rejects a graphic anywhere an expression is computed; // the property that takes one reads it out of `static_props`. Expr::Form(..) => unreachable!("a graphic is not a computed expression"), Expr::Unary(op, inner, _) => { let inner = expr_tokens(ir, inner, brush); match op { UnOp::Not => quote! { (!#inner) }, UnOp::Neg => quote! { (-#inner) }, } } Expr::Binary(op, lhs, rhs, _) => { let lhs = expr_tokens(ir, lhs, false); let rhs = expr_tokens(ir, rhs, false); let op = match op { BinOp::Add => quote! { + }, BinOp::Sub => quote! { - }, BinOp::Mul => quote! { * }, BinOp::Div => quote! { / }, BinOp::Rem => quote! { % }, BinOp::Lt => quote! { < }, BinOp::Le => quote! { <= }, BinOp::Gt => quote! { > }, BinOp::Ge => quote! { >= }, BinOp::Eq => quote! { == }, BinOp::Ne => quote! { != }, BinOp::And => quote! { && }, BinOp::Or => quote! { || }, }; quote! { (#lhs #op #rhs) } } Expr::If(cond, then, otherwise, _) => { let cond = expr_tokens(ir, cond, false); let then = expr_tokens(ir, then, brush); let otherwise = expr_tokens(ir, otherwise, brush); quote! { (if #cond { #then } else { #otherwise }) } } } } /// A prop or state read by name — both are signals in scope by their snake /// names, so a read is a tracked `.get()` either way. fn name_tokens(ir: &Ir, name: &str) -> TokenStream { let _ = ir; // Inside invocation-wire arguments, `payload` is the delivered value, // bound as a plain local (validation confines it to those positions). if name == "payload" { return quote! { __payload.clone() }; } // Props, states, and `for` loop variables are all signal-backed locals // under their snake names wherever expressions are emitted; validation // resolved every name against the file's scope. A dotted name reads a // field of a record-typed loop variable (the `.get()` clones the // row-sized record; fine until a profile says otherwise). let mut segments = name.split('.'); let head = snake_ident(segments.next().expect("non-empty name")); match segments.next() { Some(field) => { let field = snake_ident(field); quote! { #head.get().#field } } None => quote! { #head.get() }, } } fn event_kind_tokens(event: EventProp) -> TokenStream { match event { EventProp::Click => quote! { ::guiduck::core::EventKind::Click }, EventProp::CountedClick => quote! { ::guiduck::core::EventKind::CountedClick }, EventProp::PointerEnter => quote! { ::guiduck::core::EventKind::PointerEnter }, EventProp::PointerLeave => quote! { ::guiduck::core::EventKind::PointerLeave }, EventProp::PointerDown => quote! { ::guiduck::core::EventKind::PointerDown }, EventProp::PointerUp => quote! { ::guiduck::core::EventKind::PointerUp }, EventProp::Changed => quote! { ::guiduck::core::EventKind::Changed }, EventProp::Toggled => quote! { ::guiduck::core::EventKind::Toggled }, EventProp::Select => quote! { ::guiduck::core::EventKind::Select }, EventProp::Close => quote! { ::guiduck::core::EventKind::Close }, EventProp::Link => quote! { ::guiduck::core::EventKind::Link }, EventProp::FileDrop => quote! { ::guiduck::core::EventKind::FileDrop }, EventProp::FocusChange => quote! { ::guiduck::core::EventKind::FocusChange }, EventProp::Key => quote! { ::guiduck::core::EventKind::Key }, EventProp::ValueChanged => quote! { ::guiduck::core::EventKind::ValueChanged }, EventProp::Scrolled => quote! { ::guiduck::core::EventKind::Scrolled }, } } fn style_tokens(style: &StyleIr) -> TokenStream { let mut fields = TokenStream::new(); if style.width.is_some() || style.height.is_some() { let width = dim_tokens(style.width); let height = dim_tokens(style.height); fields.extend(quote! { size: ::guiduck::core::taffy::Size { width: #width, height: #height }, }); } if let Some(padding) = style.padding { fields.extend(quote! { padding: ::guiduck::core::taffy::Rect::length(#padding), }); } if let Some(gap) = style.gap { fields.extend(quote! { gap: ::guiduck::core::taffy::Size { width: ::guiduck::core::taffy::prelude::length(#gap), height: ::guiduck::core::taffy::prelude::length(#gap), }, }); } if let Some(direction) = style.direction { let dir = match direction { DirectionIr::Row => quote! { Row }, DirectionIr::Column => quote! { Column }, }; fields.extend(quote! { flex_direction: ::guiduck::core::taffy::FlexDirection::#dir, }); } if let Some(align) = style.align_items { let value = align_tokens(align); fields.extend(quote! { align_items: ::core::option::Option::Some(#value), }); } if let Some(justify) = style.justify_content { let value = justify_tokens(justify); fields.extend(quote! { justify_content: ::core::option::Option::Some(#value), }); } if let Some(align) = style.align_self { let value = align_tokens(align); fields.extend(quote! { align_self: ::core::option::Option::Some(#value), }); } if let Some(grow) = style.grow { fields.extend(quote! { flex_grow: #grow, }); } if let Some(shrink) = style.shrink { fields.extend(quote! { flex_shrink: #shrink, }); } if let Some(basis) = style.basis { let value = dim_tokens(Some(basis)); fields.extend(quote! { flex_basis: #value, }); } quote! { ::guiduck::core::taffy::Style { #fields ..::core::default::Default::default() } } } /// Field assignments applying an IR style onto an existing `__style` — /// the codegen twin of the interpreter's `apply_style_ir`, emitting only /// the fields the file gave. fn style_patch_tokens(style: &StyleIr) -> TokenStream { let mut out = TokenStream::new(); if style.width.is_some() { let width = dim_tokens(style.width); out.extend(quote! { __style.size.width = #width; }); } if style.height.is_some() { let height = dim_tokens(style.height); out.extend(quote! { __style.size.height = #height; }); } if let Some(padding) = style.padding { out.extend(quote! { __style.padding = ::guiduck::core::taffy::Rect::length(#padding); }); } if let Some(gap) = style.gap { out.extend(quote! { __style.gap = ::guiduck::core::taffy::Size { width: ::guiduck::core::taffy::prelude::length(#gap), height: ::guiduck::core::taffy::prelude::length(#gap), }; }); } if let Some(direction) = style.direction { let dir = match direction { DirectionIr::Row => quote! { Row }, DirectionIr::Column => quote! { Column }, }; out.extend(quote! { __style.flex_direction = ::guiduck::core::taffy::FlexDirection::#dir; }); } if let Some(align) = style.align_items { let value = align_tokens(align); out.extend(quote! { __style.align_items = ::core::option::Option::Some(#value); }); } if let Some(justify) = style.justify_content { let value = justify_tokens(justify); out.extend(quote! { __style.justify_content = ::core::option::Option::Some(#value); }); } if let Some(align) = style.align_self { let value = align_tokens(align); out.extend(quote! { __style.align_self = ::core::option::Option::Some(#value); }); } if let Some(grow) = style.grow { out.extend(quote! { __style.flex_grow = #grow; }); } if let Some(shrink) = style.shrink { out.extend(quote! { __style.flex_shrink = #shrink; }); } if let Some(basis) = style.basis { let value = dim_tokens(Some(basis)); out.extend(quote! { __style.flex_basis = #value; }); } out } fn dim_tokens(dim: Option) -> TokenStream { match dim { Some(DimIr::Px(v)) => quote! { ::guiduck::core::taffy::prelude::length(#v) }, Some(DimIr::Percent(v)) => quote! { ::guiduck::core::taffy::prelude::percent(#v) }, Some(DimIr::Auto) | None => quote! { ::guiduck::core::taffy::prelude::auto() }, } } fn align_tokens(align: AlignIr) -> TokenStream { match align { AlignIr::Start => quote! { ::guiduck::core::taffy::AlignItems::FLEX_START }, AlignIr::End => quote! { ::guiduck::core::taffy::AlignItems::FLEX_END }, AlignIr::Center => quote! { ::guiduck::core::taffy::AlignItems::CENTER }, AlignIr::Stretch => quote! { ::guiduck::core::taffy::AlignItems::STRETCH }, } } fn justify_tokens(align: AlignIr) -> TokenStream { match align { AlignIr::Start => quote! { ::guiduck::core::taffy::JustifyContent::FLEX_START }, AlignIr::End => quote! { ::guiduck::core::taffy::JustifyContent::FLEX_END }, AlignIr::Center => quote! { ::guiduck::core::taffy::JustifyContent::CENTER }, AlignIr::Stretch => quote! { ::guiduck::core::taffy::JustifyContent::STRETCH }, } } fn type_tokens(records: &[IrRecord], ty: ValueTy) -> TokenStream { let element = elem_type_tokens(records, ty.kind); if ty.list { quote! { ::std::vec::Vec<#element> } } else { element } } fn elem_type_tokens(records: &[IrRecord], kind: ElemKind) -> TokenStream { match kind { ElemKind::Scalar(kind) => scalar_type_tokens(kind), ElemKind::Record(index) => { let name = format_ident!("{}", records[index as usize].name); quote! { #name } } } } fn scalar_type_tokens(kind: TypeKind) -> TokenStream { match kind { TypeKind::I32 => quote! { i32 }, TypeKind::I64 => quote! { i64 }, TypeKind::F32 => quote! { f32 }, TypeKind::F64 => quote! { f64 }, TypeKind::Bool => quote! { bool }, TypeKind::String => quote! { ::std::string::String }, } } /// A literal, optionally coerced to a declared type. fn literal_tokens(literal: &Literal, ty: Option) -> TokenStream { if let Literal::List(items) = literal { let element = ty.map(|ty| ValueTy { kind: ty.kind, list: false, }); let items = items.iter().map(|item| literal_tokens(item, element)); return quote! { ::std::vec![#(#items),*] }; } if let Literal::Record { name, fields } = literal { let name = format_ident!("{name}"); let fields = fields.iter().map(|(field, value)| { let field = snake_ident(field); // Field value coercion rides the generated struct's field // types; emit uncoerced and let inference (and From) settle it. let value = literal_tokens(value, None); quote! { #field: #value, } }); return quote! { #name { #(#fields)* } }; } let ty = ty.and_then(|ty| match ty.kind { ElemKind::Scalar(kind) => Some(kind), ElemKind::Record(_) => None, }); match (literal, ty) { (Literal::Int(v), Some(TypeKind::F32)) => { let v = *v as f32; quote! { #v } } (Literal::Int(v), Some(TypeKind::F64)) => { let v = *v as f64; quote! { #v } } (Literal::Int(v), Some(TypeKind::I32)) => { let v = *v as i32; quote! { #v } } (Literal::Int(v), _) => quote! { #v }, (Literal::Float(v), Some(TypeKind::F32)) => { let v = *v as f32; quote! { #v } } (Literal::Float(v), _) => quote! { #v }, (Literal::Bool(v), _) => quote! { #v }, (Literal::Str(v), _) => quote! { ::std::string::String::from(#v) }, (Literal::List(..) | Literal::Record { .. }, _) => unreachable!("handled above"), (Literal::Path(_) | Literal::Asset(_) | Literal::Rich(_) | Literal::ScrollAxes(_), _) => { // These reach codegen through `prop_literal_tokens`, which // dispatches on the property's own type — never through the // generic scalar path. unreachable!("not a scalar literal") } (Literal::Color(r, g, b, a), _) => { quote! { ::guiduck::paint::Color::from_rgba8(#r, #g, #b, #a) } } } } /// kebab-case → snake_case identifier. fn snake_ident(name: &str) -> proc_macro2::Ident { format_ident!("{}", name.replace('-', "_")) }