facett-core 0.1.11

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **The canonical Elm contract** — the reusable scaffold behind FC-2 / FC-9.
//!
//! `facett-security` hand-rolled the pattern first (see its `lib.rs`): all state
//! in one serializable `Model`, every input a `Msg`, mutation only in
//! `update(&mut self, Msg) -> Vec<Effect>`, and a **pure** `render(&self) -> Vec<Msg>`
//! view that paints and *returns* the `Msg`s it produced — with a tiny `ui()`
//! bridge that applies them through `update`. This module lifts that bridge into
//! `facett-core` so the ~28 remaining components adopt it instead of re-deriving it.
//!
//! ## What you get
//! - [`Elm`] — the trait (associated `Model` / `Msg` / `Effect`; methods
//!   [`state`](Elm::state) / [`update`](Elm::update) / [`view`](Elm::view) /
//!   [`title`](Elm::title)). Implement this once per component.
//! - [`impl_facet_via_elm!`](crate::impl_facet_via_elm) — the bridge macro. It
//!   writes the `impl Facet for YourView` for you (`title` + the pure-view `ui`
//!   loop + a serde `state_json`). **A blanket `impl<T: Elm> Facet for T` is
//!   impossible across crates (orphan rule) and would collide with the existing
//!   hand-written `impl Facet` blocks mid-migration** — so each component crate
//!   invokes this macro locally on its concrete type instead.
//! - [`crate::harness`] — a headless driver: apply a `Vec<Msg>`, snapshot
//!   `state()`, no egui / no GPU. That turns FC-2 into a *tested* property.
//!
//! ## Adopting it in a component crate
//! ```ignore
//! use facett_core::Elm;
//!
//! impl Elm for MyView {
//!     type Model = MyState;   // Serialize + DeserializeOwned + Clone + PartialEq
//!     type Msg = MyMsg;       // Clone + Debug
//!     type Effect = MyEffect; // often an uninhabited `enum MyEffect {}` (no I/O)
//!     fn title(&self) -> &str { &self.title }
//!     fn state(&self) -> &MyState { &self.state }
//!     fn update(&mut self, msg: MyMsg) -> Vec<MyEffect> { /* the ONLY mutation path */ }
//!     fn view(&self, ui: &mut egui::Ui) -> Vec<MyMsg> { /* pure: paint + return Msgs */ }
//! }
//!
//! // Writes `impl Facet for MyView` (title + view/update bridge + state_json):
//! facett_core::impl_facet_via_elm!(MyView);
//! ```

use serde::Serialize;
use serde::de::DeserializeOwned;

/// The canonical Model / Msg / Effect / pure-view split the component contract
/// (`.nornir/component-contract.md` §3) requires. Implement it once per component;
/// [`impl_facet_via_elm!`](crate::impl_facet_via_elm) turns it into a [`Facet`](crate::Facet).
///
/// The bounds encode the FCs directly:
/// - `Model: Serialize + DeserializeOwned + Clone + PartialEq` — FC-3 (readable,
///   round-trippable observable state).
/// - `Msg: Clone + Debug` — FC-2 (a writable, inspectable input surface).
/// - `Effect` — FC-8 side work *as data*; an uninhabited `enum Effect {}` is the
///   honest statement that a component does no I/O.
pub trait Elm {
    /// All observable state, in one serializable struct (FC-1 / FC-3).
    type Model: Serialize + DeserializeOwned + Clone + PartialEq;
    /// Every input the component accepts (FC-2). The **only** legal way to mutate
    /// the [`Model`](Elm::Model).
    type Msg: Clone + std::fmt::Debug;
    /// Side work as data (FC-8). Use `enum Effect {}` when the component does no I/O.
    type Effect;

    /// The deck keys facets off this (mirrors [`Facet::title`](crate::Facet::title)).
    fn title(&self) -> &str;

    /// **FC-3** — read the complete observable state at any frame boundary.
    fn state(&self) -> &Self::Model;

    /// **FC-2 / FC-8** — the single mutation path. Apply one [`Msg`](Elm::Msg);
    /// return the [`Effect`](Elm::Effect)s the host should run (empty = nothing to do).
    fn update(&mut self, msg: Self::Msg) -> Vec<Self::Effect>;

    /// **FC-9 / FC-4** — a **pure** function of `&self`: it paints and *returns*
    /// the [`Msg`](Elm::Msg)s it produced. It MUST NOT mutate the model or do I/O;
    /// the [`Facet::ui`](crate::Facet::ui) bridge applies the returned messages
    /// through [`update`](Elm::update).
    fn view(&self, ui: &mut egui::Ui) -> Vec<Self::Msg>;
}

/// Crate paths the [`impl_facet_via_elm!`](crate::impl_facet_via_elm) expansion
/// reaches through, so a downstream crate needs only `facett-core` in scope (not
/// its own `egui` / `serde_json` idents) for the macro to resolve. Hidden: not a
/// stable API, only the macro should touch it.
#[doc(hidden)]
pub mod __macro_deps {
    pub use egui;
    pub use serde_json;
}

/// Write the `impl Facet for $ty` for a type that already implements
/// [`Elm`](crate::Elm) — the reusable version of the bridge `facett-security`
/// hand-writes.
///
/// This is a **macro, not a blanket impl**, on purpose: `impl<T: Elm> Facet for T`
/// cannot live in `facett-core` and also be added by downstream crates (orphan
/// rule), and it would collide with every still-hand-written `impl Facet` during
/// the staged migration. Invoking the macro locally sidesteps both.
///
/// # Three forms
/// ```ignore
/// // 1. Just the core bridge: title + pure-view `ui` loop + serde `state_json`.
/// facett_core::impl_facet_via_elm!(MyView);
///
/// // 2. Core bridge PLUS extra `Facet` overrides (caps, scale, selection_json,
/// //    copy, …) spliced into the SAME impl block. NOTE: form 2 still emits the
/// //    default serde `state_json`, so the extra block must NOT define its own
/// //    `state_json` (that would be a duplicate-method error) — use form 3 for that:
/// facett_core::impl_facet_via_elm!(MyView, {
///     fn caps(&self) -> facett_core::FacetCaps {
///         facett_core::FacetCaps::NONE.selectable().scalable()
///     }
///     fn scale(&self) -> f32 { self.state().scale }
///     fn set_scale(&mut self, s: f32) { let _ = self.update(MyMsg::SetScale(s)); }
/// });
///
/// // 3. Component publishes a RICHER `state_json` than plain `serde(state())`:
/// //    pass `custom_state_json` to SUPPRESS the default, then provide your own
/// //    `state_json` (and any other overrides) in the extra block. This is what
/// //    lets rich components (helix, timeline, security, …) use the macro instead
/// //    of hand-writing the whole `impl Facet`.
/// facett_core::impl_facet_via_elm!(MyView, custom_state_json, {
///     fn state_json(&self) -> serde_json::Value {
///         // e.g. the model PLUS derived introspection keys the deck/matrix read
///         serde_json::json!({ "bar_count": self.state().bars.len(), /* … */ })
///     }
///     fn caps(&self) -> facett_core::FacetCaps { facett_core::FacetCaps::NONE.selectable() }
/// });
/// ```
/// The generated `ui` is the FC-9 loop verbatim: `for m in self.view(ui) { self.update(m); }`.
#[macro_export]
macro_rules! impl_facet_via_elm {
    ($ty:ty) => {
        $crate::impl_facet_via_elm!($ty, {});
    };
    // Form 2: default serde `state_json` + caller's extra overrides (no `state_json`).
    ($ty:ty, { $($extra:item)* }) => {
        impl $crate::Facet for $ty {
            fn title(&self) -> &str {
                <Self as $crate::Elm>::title(self)
            }

            /// FC-9: the view is pure (paints + emits `Msg`s); FC-2: mutation only
            /// via `update`. The immediate-mode trait just bridges the two.
            fn ui(&mut self, ui: &mut $crate::elm::__macro_deps::egui::Ui) {
                let msgs = <Self as $crate::Elm>::view(self, ui);
                for m in msgs {
                    let _ = <Self as $crate::Elm>::update(self, m);
                }
            }

            fn state_json(&self) -> $crate::elm::__macro_deps::serde_json::Value {
                $crate::elm::__macro_deps::serde_json::to_value(<Self as $crate::Elm>::state(self))
                    .unwrap_or($crate::elm::__macro_deps::serde_json::Value::Null)
            }

            $($extra)*
        }
    };
    // Form 3: `custom_state_json` marker SUPPRESSES the default `state_json`; the
    // caller's extra block supplies its own (plus any other overrides). This is the
    // form rich components need — no duplicate-method clash.
    ($ty:ty, custom_state_json, { $($extra:item)* }) => {
        impl $crate::Facet for $ty {
            fn title(&self) -> &str {
                <Self as $crate::Elm>::title(self)
            }

            /// FC-9: the view is pure (paints + emits `Msg`s); FC-2: mutation only
            /// via `update`. The immediate-mode trait just bridges the two.
            fn ui(&mut self, ui: &mut $crate::elm::__macro_deps::egui::Ui) {
                let msgs = <Self as $crate::Elm>::view(self, ui);
                for m in msgs {
                    let _ = <Self as $crate::Elm>::update(self, m);
                }
            }

            $($extra)*
        }
    };
}