//! Runtime support for generated component code, including the dynamic //! bridge the runtime interpreter dispatches through. //! //! In dev (hot-reload) mode, structure and bindings are interpreted from the //! current file content, but handlers are always compiled Rust: the codegen //! emits a [`DynHandlers`] impl — a name→method registry that reassembles //! the component's typed `Cx` from a [`DynCx`] at each invocation. If a //! reload changed the file so a state the compiled logic expects is missing //! or retyped, the invocation is skipped with a warning instead of //! panicking; a rebuild restores full typing. use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use guiduck_signals::Signal; /// A typed output signal of a component: the component's logic emits values, /// the embedding code subscribes. /// /// Cloning shares the subscriber list, so generated code can hold one clone /// in the component struct and move others into event closures. pub struct Emitter { subscribers: Rc>>>, } impl Emitter { pub fn new() -> Self { Self { subscribers: Rc::new(RefCell::new(Vec::new())), } } /// Deliver a value to every subscriber. pub fn emit(&self, value: &T) { for subscriber in self.subscribers.borrow_mut().iter_mut() { subscriber(value); } } /// Register a callback for emitted values. pub fn subscribe(&self, f: impl FnMut(&T) + 'static) { self.subscribers.borrow_mut().push(Box::new(f)); } } impl Clone for Emitter { fn clone(&self) -> Self { Self { subscribers: self.subscribers.clone(), } } } impl Default for Emitter { fn default() -> Self { Self::new() } } /// A state signal with its type erased into the closed `.gdc` type set. #[derive(Copy, Clone)] pub enum DynSignal { I32(Signal), I64(Signal), F32(Signal), F64(Signal), Bool(Signal), Str(Signal), ListI32(Signal>), ListI64(Signal>), ListF32(Signal>), ListF64(Signal>), ListBool(Signal>), ListStr(Signal>), /// A `(List RecordName)` state in dev mode: elements are /// [`DynValue::Record`]s. The typed handler bridge converts to and from /// the generated struct per invocation. RecordList(Signal>), /// A record-valued `for` loop variable in dev mode. RecordValue(Signal), } /// An output emitter with its type erased. #[derive(Clone)] pub enum DynEmitter { I32(Emitter), I64(Emitter), F32(Emitter), F64(Emitter), Bool(Emitter), Str(Emitter), } /// A prop value snapshot for interpreter expressions. #[derive(Clone, Debug, PartialEq)] pub enum DynValue { I64(i64), F64(f64), Bool(bool), Str(String), /// A `(List T)` value; elements share one shape. List(Vec), /// A record value: field names carried (self-describing, so the dyn /// side never needs the generated struct's layout). Record(Vec<(String, DynValue)>), } /// The dynamic capability surface of a mounted component instance: prop and /// state signals and output emitters by name. Built by the interpreter; /// consumed by interpreted bindings and by the generated [`DynHandlers`] /// registry. Props are signal-backed, so interpreted expressions track prop /// reads and a parent component can drive them after mount. #[derive(Clone, Default)] pub struct DynCx { pub signals: HashMap, pub emitters: HashMap, pub props: HashMap, } macro_rules! dyn_getters { ($($signal_fn:ident, $prop_fn:ident, $emitter_fn:ident, $variant:ident, $ty:ty;)*) => { $( pub fn $signal_fn(&self, name: &str) -> Option> { match self.signals.get(name) { Some(DynSignal::$variant(signal)) => Some(*signal), _ => None, } } pub fn $prop_fn(&self, name: &str) -> Option> { match self.props.get(name) { Some(DynSignal::$variant(signal)) => Some(*signal), _ => None, } } pub fn $emitter_fn(&self, name: &str) -> Option> { match self.emitters.get(name) { Some(DynEmitter::$variant(emitter)) => Some(emitter.clone()), _ => None, } } )* }; } macro_rules! dyn_list_getters { ($($signal_fn:ident, $prop_fn:ident, $variant:ident, $ty:ty;)*) => { $( pub fn $signal_fn(&self, name: &str) -> Option>> { match self.signals.get(name) { Some(DynSignal::$variant(signal)) => Some(*signal), _ => None, } } pub fn $prop_fn(&self, name: &str) -> Option>> { match self.props.get(name) { Some(DynSignal::$variant(signal)) => Some(*signal), _ => None, } } )* }; } impl DynCx { dyn_getters! { signal_i32, prop_i32, emitter_i32, I32, i32; signal_i64, prop_i64, emitter_i64, I64, i64; signal_f32, prop_f32, emitter_f32, F32, f32; signal_f64, prop_f64, emitter_f64, F64, f64; signal_bool, prop_bool, emitter_bool, Bool, bool; signal_string, prop_string, emitter_string, Str, String; } pub fn signal_list_record(&self, name: &str) -> Option>> { match self.signals.get(name) { Some(DynSignal::RecordList(signal)) => Some(*signal), _ => None, } } dyn_list_getters! { signal_list_i32, prop_list_i32, ListI32, i32; signal_list_i64, prop_list_i64, ListI64, i64; signal_list_f32, prop_list_f32, ListF32, f32; signal_list_f64, prop_list_f64, ListF64, f64; signal_list_bool, prop_list_bool, ListBool, bool; signal_list_string, prop_list_string, ListStr, String; } } /// The compiled half of a dev-mounted component: dispatches handler /// invocations by name into the typed logic. Generated per component. pub trait DynHandlers { /// Invoke a handler. `args` carries the handler's payloads in order — /// one value for widget payload-carrying events (`:on-change`), the /// evaluated invocation arguments for wires like /// `:on-click (remove todo.id)`, empty for plain events. /// /// Returns whether the handler consumed the event (called `cx.consume()`), /// so the interpreter can stop propagation exactly as the compiled path /// does. A skipped or unknown handler consumes nothing. fn invoke(&mut self, name: &str, cx: &DynCx, args: &[DynValue]) -> bool; /// Install a widget **query** — a `:get-image (load …)` wire — whose handler /// returns a value rather than being dispatched as an event. /// /// The interpreter cannot do this itself: the resolver's return type and the /// widget's setter both belong to the widget's crate, which the erased /// interpreter cannot name. So the generated (typed) implementation does the /// work — downcast `widget` to its concrete type and install a resolver that /// calls the compiled handler, reassembling the typed context from `cx` at /// resolve time — while the interpreter only routes the call here, keyed by /// the handler `name`. The default does nothing, which is correct for every /// component that declares no query. fn install_query(&self, name: &str, widget: &mut dyn crate::widget::Widget, cx: &Rc) { let _ = (name, widget, cx); } /// Run the component's mount hook, once its subtree is built and wired. /// /// The hook is where an application seeds state that comes from outside /// the component language, so it has to run on this path too or a live /// mount would show an empty component where the compiled one shows a /// loaded document. As with [`Self::invoke`], the generated implementation /// reassembles the typed context from `cx` and calls the compiled method; /// the default does nothing, which is correct for a component that /// declares no hook. fn mounted(&mut self, cx: &DynCx) { let _ = cx; } } /// Everything the interpreter needs from the compiled side: the handler /// registry, the typed props snapshotted as dynamic values (initial values /// for the prop signals), a spec factory per instantiated component type /// (so nested instances keep compiled handlers at every depth), and the /// component, widget, and asset search paths the typed build resolved against — /// baked in as absolute directories so a dev mount resolves `.gdc` references, /// `.gdw` manifests, and `(asset "…")` references exactly the way the proc-macro /// did. /// /// Widgets themselves the interpreter builds from the process-wide /// [`registered_widgets`](crate::registered_widgets) link-time registry, not /// from here — the builtins and every application widget register into it, so a /// `container` and a `markdown` are built the same way, and a dev edit adding a /// widget works as long as that widget is linked. #[derive(Clone)] pub struct ComponentSpec { pub handlers: Rc>, pub props: Vec<(String, DynValue)>, pub children: HashMap ComponentSpec>>, pub search_path: Vec, /// Where `.gdw` widget manifests are read from, so a dev mount builds the /// same vocabulary the proc-macro compiled against. pub widget_path: Vec, /// Where `(asset "…")` references resolve. The typed build embeds asset /// bytes at compile time; a dev mount reads the same files from disk, so /// editing an asset shows up without a rebuild. pub asset_path: Vec, } /// Dev-mode diagnostic for a component instance that could not be built /// (the live file names a component the compiled side does not know). pub fn warn_component_skipped(component: &str, child: &str) { eprintln!( "guiduck[dev]: skipped `{child}` inside `{component}`: the compiled logic \ does not instantiate it (rebuild to restore)" ); } /// Dev-mode diagnostic for a `.gdw`-declared widget the binary has no factory /// for (the live file names a widget whose manifest was not on the typed /// build's widget path). pub fn warn_widget_skipped(component: &str, widget: &str) { eprintln!( "guiduck[dev]: `{widget}` inside `{component}` draws as an empty box: this \ binary has no factory for it, because it was not in the widget \ vocabulary when it was built (rebuild to restore)" ); } /// Dev-mode diagnostic for a handler invocation that could not be bridged /// (e.g. the live file no longer declares a state the compiled logic needs). pub fn warn_handler_skipped(component: &str, handler: &str) { eprintln!( "guiduck[dev]: skipped handler `{handler}` on `{component}`: the live file no \ longer matches the compiled logic (rebuild to restore); or the handler is unknown" ); }