facett-core 0.1.1

facett — visual kernel: render a node/edge Scene into egui (wgpu fast path to come)
Documentation
//! **Uniform capability model** — every [`Facet`](crate::Facet) returns a small
//! [`FacetCaps`] descriptor so a host (the [`FacetDeck`](crate::FacetDeck), korp,
//! nornir) can treat all components uniformly: query "is this scalable?",
//! "copyable?", "searchable?" without knowing the concrete type. See
//! `.nornir/design/capability-model.md`.

/// What a Facet can do, so hosts treat every component uniformly.
/// All-false by default: a facet opts in to each capability it actually honors.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FacetCaps {
    /// Honors a uniform `scale: f32` (Ctrl-+/Ctrl-- and a toolbar zoom).
    pub scalable: bool,
    /// Has a current selection that can be copied (drives Ctrl-C/Ctrl-X gating).
    pub copyable: bool,
    /// Accepts pasted content (drives Ctrl-V gating).
    pub pasteable: bool,
    /// Supports cut (remove-on-copy). Implies `copyable`.
    pub cuttable: bool,
    /// Has a user selection model (rows, nodes, points…).
    pub selectable: bool,
    /// Reads the facett Theme from the context (vs. fixed colours).
    pub themeable: bool,
    /// Its content area can be resized by the host without breaking.
    pub resizable: bool,
    /// Exposes a text search/filter over its content.
    pub searchable: bool,
    /// Supports inline cell editing (Enter/double-click to enter edit mode,
    /// Enter/Tab to commit, Escape to cancel). Emits change events via GridResponse.
    pub editable: bool,
}

impl Default for FacetCaps {
    fn default() -> Self {
        Self::NONE
    }
}

impl FacetCaps {
    /// Everything off — the explicit baseline (same as `default()`).
    pub const NONE: Self = Self {
        scalable: false,
        copyable: false,
        pasteable: false,
        cuttable: false,
        selectable: false,
        themeable: false,
        resizable: false,
        searchable: false,
        editable: false,
    };

    /// Everything on — handy in tests / a maximally-capable facet.
    pub const ALL: Self = Self {
        scalable: true,
        copyable: true,
        pasteable: true,
        cuttable: true,
        selectable: true,
        themeable: true,
        resizable: true,
        searchable: true,
        editable: true,
    };

    // Ergonomic builders so a facet writes `FacetCaps::NONE.scalable().themeable()`.
    pub const fn scalable(mut self) -> Self {
        self.scalable = true;
        self
    }
    pub const fn copyable(mut self) -> Self {
        self.copyable = true;
        self
    }
    pub const fn pasteable(mut self) -> Self {
        self.pasteable = true;
        self
    }
    /// Cut implies copy.
    pub const fn cuttable(mut self) -> Self {
        self.cuttable = true;
        self.copyable = true;
        self
    }
    pub const fn selectable(mut self) -> Self {
        self.selectable = true;
        self
    }
    pub const fn themeable(mut self) -> Self {
        self.themeable = true;
        self
    }
    pub const fn resizable(mut self) -> Self {
        self.resizable = true;
        self
    }
    pub const fn searchable(mut self) -> Self {
        self.searchable = true;
        self
    }
    pub const fn editable(mut self) -> Self {
        self.editable = true;
        self
    }

    /// JSON for the introspection/state dump.
    pub fn to_json(self) -> serde_json::Value {
        serde_json::json!({
            "scalable": self.scalable, "copyable": self.copyable,
            "pasteable": self.pasteable, "cuttable": self.cuttable,
            "selectable": self.selectable, "themeable": self.themeable,
            "resizable": self.resizable, "searchable": self.searchable,
            "editable": self.editable,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn none_is_all_false_and_equals_default() {
        let n = FacetCaps::NONE;
        assert!(!n.scalable && !n.copyable && !n.pasteable && !n.cuttable);
        assert!(!n.selectable && !n.themeable && !n.resizable && !n.searchable);
        assert!(!n.editable);
        assert_eq!(FacetCaps::default(), FacetCaps::NONE);
    }

    #[test]
    fn all_is_all_true() {
        let a = FacetCaps::ALL;
        assert!(a.scalable && a.copyable && a.pasteable && a.cuttable);
        assert!(a.selectable && a.themeable && a.resizable && a.searchable);
        assert!(a.editable);
    }

    #[test]
    fn builders_set_exactly_one_flag() {
        assert_eq!(FacetCaps::NONE.scalable(), FacetCaps { scalable: true, ..FacetCaps::NONE });
        assert_eq!(FacetCaps::NONE.selectable(), FacetCaps { selectable: true, ..FacetCaps::NONE });
        assert_eq!(FacetCaps::NONE.searchable(), FacetCaps { searchable: true, ..FacetCaps::NONE });
        assert_eq!(FacetCaps::NONE.editable(), FacetCaps { editable: true, ..FacetCaps::NONE });
    }

    #[test]
    fn cuttable_implies_copyable() {
        let c = FacetCaps::NONE.cuttable();
        assert!(c.cuttable && c.copyable, "cut implies copy");
    }

    #[test]
    fn builders_chain_in_const_context() {
        const C: FacetCaps = FacetCaps::NONE.selectable().copyable().searchable().scalable().resizable().editable();
        assert!(C.selectable && C.copyable && C.searchable && C.scalable && C.resizable && C.editable);
        assert!(!C.pasteable && !C.cuttable && !C.themeable);
    }

    #[test]
    fn to_json_carries_every_flag() {
        let j = FacetCaps::NONE.scalable().themeable().editable().to_json();
        assert_eq!(j["scalable"], true);
        assert_eq!(j["themeable"], true);
        assert_eq!(j["editable"], true);
        assert_eq!(j["copyable"], false);
        // all nine keys present
        assert_eq!(j.as_object().unwrap().len(), 9);
    }
}