component.rs
raw
//! 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<T> {
subscribers: Rc<RefCell<Vec<Box<dyn FnMut(&T)>>>>,
}
impl<T> Emitter<T> {
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<T> Clone for Emitter<T> {
fn clone(&self) -> Self {
Self {
subscribers: self.subscribers.clone(),
}
}
}
impl<T> Default for Emitter<T> {
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<i32>),
I64(Signal<i64>),
F32(Signal<f32>),
F64(Signal<f64>),
Bool(Signal<bool>),
Str(Signal<String>),
ListI32(Signal<Vec<i32>>),
ListI64(Signal<Vec<i64>>),
ListF32(Signal<Vec<f32>>),
ListF64(Signal<Vec<f64>>),
ListBool(Signal<Vec<bool>>),
ListStr(Signal<Vec<String>>),
/// 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<Vec<DynValue>>),
/// A record-valued `for` loop variable in dev mode.
RecordValue(Signal<DynValue>),
}
/// An output emitter with its type erased.
#[derive(Clone)]
pub enum DynEmitter {
I32(Emitter<i32>),
I64(Emitter<i64>),
F32(Emitter<f32>),
F64(Emitter<f64>),
Bool(Emitter<bool>),
Str(Emitter<String>),
}
/// 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<DynValue>),
/// 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<String, DynSignal>,
pub emitters: HashMap<String, DynEmitter>,
pub props: HashMap<String, DynSignal>,
}
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<Signal<$ty>> {
match self.signals.get(name) {
Some(DynSignal::$variant(signal)) => Some(*signal),
_ => None,
}
}
pub fn $prop_fn(&self, name: &str) -> Option<Signal<$ty>> {
match self.props.get(name) {
Some(DynSignal::$variant(signal)) => Some(*signal),
_ => None,
}
}
pub fn $emitter_fn(&self, name: &str) -> Option<Emitter<$ty>> {
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<Signal<Vec<$ty>>> {
match self.signals.get(name) {
Some(DynSignal::$variant(signal)) => Some(*signal),
_ => None,
}
}
pub fn $prop_fn(&self, name: &str) -> Option<Signal<Vec<$ty>>> {
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<Signal<Vec<DynValue>>> {
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<DynCx>) {
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<RefCell<dyn DynHandlers>>,
pub props: Vec<(String, DynValue)>,
pub children: HashMap<String, Rc<dyn Fn() -> ComponentSpec>>,
pub search_path: Vec<std::path::PathBuf>,
/// Where `.gdw` widget manifests are read from, so a dev mount builds the
/// same vocabulary the proc-macro compiled against.
pub widget_path: Vec<std::path::PathBuf>,
/// 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<std::path::PathBuf>,
}
/// 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"
);
}