Skip to main content

facett_core/
caps.rs

1//! **Uniform capability model** — every [`Facet`](crate::Facet) returns a small
2//! [`FacetCaps`] descriptor so a host (the [`FacetDeck`](crate::FacetDeck), korp,
3//! nornir) can treat all components uniformly: query "is this scalable?",
4//! "copyable?", "searchable?" without knowing the concrete type. See
5//! `.nornir/design/capability-model.md`.
6
7/// What a Facet can do, so hosts treat every component uniformly.
8/// All-false by default: a facet opts in to each capability it actually honors.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct FacetCaps {
11    /// Honors a uniform `scale: f32` (Ctrl-+/Ctrl-- and a toolbar zoom).
12    pub scalable: bool,
13    /// Has a current selection that can be copied (drives Ctrl-C/Ctrl-X gating).
14    pub copyable: bool,
15    /// Accepts pasted content (drives Ctrl-V gating).
16    pub pasteable: bool,
17    /// Supports cut (remove-on-copy). Implies `copyable`.
18    pub cuttable: bool,
19    /// Has a user selection model (rows, nodes, points…).
20    pub selectable: bool,
21    /// Reads the facett Theme from the context (vs. fixed colours).
22    pub themeable: bool,
23    /// Its content area can be resized by the host without breaking.
24    pub resizable: bool,
25    /// Exposes a text search/filter over its content.
26    pub searchable: bool,
27}
28
29impl Default for FacetCaps {
30    fn default() -> Self {
31        Self::NONE
32    }
33}
34
35impl FacetCaps {
36    /// Everything off — the explicit baseline (same as `default()`).
37    pub const NONE: Self = Self {
38        scalable: false,
39        copyable: false,
40        pasteable: false,
41        cuttable: false,
42        selectable: false,
43        themeable: false,
44        resizable: false,
45        searchable: false,
46    };
47
48    /// Everything on — handy in tests / a maximally-capable facet.
49    pub const ALL: Self = Self {
50        scalable: true,
51        copyable: true,
52        pasteable: true,
53        cuttable: true,
54        selectable: true,
55        themeable: true,
56        resizable: true,
57        searchable: true,
58    };
59
60    // Ergonomic builders so a facet writes `FacetCaps::NONE.scalable().themeable()`.
61    pub const fn scalable(mut self) -> Self {
62        self.scalable = true;
63        self
64    }
65    pub const fn copyable(mut self) -> Self {
66        self.copyable = true;
67        self
68    }
69    pub const fn pasteable(mut self) -> Self {
70        self.pasteable = true;
71        self
72    }
73    /// Cut implies copy.
74    pub const fn cuttable(mut self) -> Self {
75        self.cuttable = true;
76        self.copyable = true;
77        self
78    }
79    pub const fn selectable(mut self) -> Self {
80        self.selectable = true;
81        self
82    }
83    pub const fn themeable(mut self) -> Self {
84        self.themeable = true;
85        self
86    }
87    pub const fn resizable(mut self) -> Self {
88        self.resizable = true;
89        self
90    }
91    pub const fn searchable(mut self) -> Self {
92        self.searchable = true;
93        self
94    }
95
96    /// JSON for the introspection/state dump.
97    pub fn to_json(self) -> serde_json::Value {
98        serde_json::json!({
99            "scalable": self.scalable, "copyable": self.copyable,
100            "pasteable": self.pasteable, "cuttable": self.cuttable,
101            "selectable": self.selectable, "themeable": self.themeable,
102            "resizable": self.resizable, "searchable": self.searchable,
103        })
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn none_is_all_false_and_equals_default() {
113        let n = FacetCaps::NONE;
114        assert!(!n.scalable && !n.copyable && !n.pasteable && !n.cuttable);
115        assert!(!n.selectable && !n.themeable && !n.resizable && !n.searchable);
116        assert_eq!(FacetCaps::default(), FacetCaps::NONE);
117    }
118
119    #[test]
120    fn all_is_all_true() {
121        let a = FacetCaps::ALL;
122        assert!(a.scalable && a.copyable && a.pasteable && a.cuttable);
123        assert!(a.selectable && a.themeable && a.resizable && a.searchable);
124    }
125
126    #[test]
127    fn builders_set_exactly_one_flag() {
128        assert_eq!(FacetCaps::NONE.scalable(), FacetCaps { scalable: true, ..FacetCaps::NONE });
129        assert_eq!(FacetCaps::NONE.selectable(), FacetCaps { selectable: true, ..FacetCaps::NONE });
130        assert_eq!(FacetCaps::NONE.searchable(), FacetCaps { searchable: true, ..FacetCaps::NONE });
131    }
132
133    #[test]
134    fn cuttable_implies_copyable() {
135        let c = FacetCaps::NONE.cuttable();
136        assert!(c.cuttable && c.copyable, "cut implies copy");
137    }
138
139    #[test]
140    fn builders_chain_in_const_context() {
141        const C: FacetCaps = FacetCaps::NONE.selectable().copyable().searchable().scalable().resizable();
142        assert!(C.selectable && C.copyable && C.searchable && C.scalable && C.resizable);
143        assert!(!C.pasteable && !C.cuttable && !C.themeable);
144    }
145
146    #[test]
147    fn to_json_carries_every_flag() {
148        let j = FacetCaps::NONE.scalable().themeable().to_json();
149        assert_eq!(j["scalable"], true);
150        assert_eq!(j["themeable"], true);
151        assert_eq!(j["copyable"], false);
152        // all eight keys present
153        assert_eq!(j.as_object().unwrap().len(), 8);
154    }
155}