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"
    );
}