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    /// Supports inline cell editing (Enter/double-click to enter edit mode,
28    /// Enter/Tab to commit, Escape to cancel). Emits change events via GridResponse.
29    pub editable: bool,
30    /// Drives the shared [`Navigable`](crate::nav::Navigable) pan/zoom/fit model
31    /// (wheel zoom-to-cursor, drag-pan, keyboard pan/zoom, fit-to-view). A host
32    /// can offer navigation help / a fit affordance when this is set.
33    pub navigable: bool,
34    /// Its items can be **dragged and dropped** onto zones (cards→columns,
35    /// bars→lanes) via the shared [`dragdrop`](crate::dragdrop) primitive. A host
36    /// can offer drag affordances / a "moves pending" indicator when set.
37    pub draggable: bool,
38    /// Lays its content on a **shared time axis** ([`time_axis`](crate::time_axis))
39    /// that can be panned/zoomed in time (gantt, CFD, calendar, timeline). A host
40    /// can link several such facets to one axis and offer a time-scrubber.
41    pub time_scrollable: bool,
42}
43
44impl Default for FacetCaps {
45    fn default() -> Self {
46        Self::NONE
47    }
48}
49
50impl FacetCaps {
51    /// Everything off — the explicit baseline (same as `default()`).
52    pub const NONE: Self = Self {
53        scalable: false,
54        copyable: false,
55        pasteable: false,
56        cuttable: false,
57        selectable: false,
58        themeable: false,
59        resizable: false,
60        searchable: false,
61        editable: false,
62        navigable: false,
63        draggable: false,
64        time_scrollable: false,
65    };
66
67    /// Everything on — handy in tests / a maximally-capable facet.
68    pub const ALL: Self = Self {
69        scalable: true,
70        copyable: true,
71        pasteable: true,
72        cuttable: true,
73        selectable: true,
74        themeable: true,
75        resizable: true,
76        searchable: true,
77        editable: true,
78        navigable: true,
79        draggable: true,
80        time_scrollable: true,
81    };
82
83    // Ergonomic builders so a facet writes `FacetCaps::NONE.scalable().themeable()`.
84    pub const fn scalable(mut self) -> Self {
85        self.scalable = true;
86        self
87    }
88    pub const fn copyable(mut self) -> Self {
89        self.copyable = true;
90        self
91    }
92    pub const fn pasteable(mut self) -> Self {
93        self.pasteable = true;
94        self
95    }
96    /// Cut implies copy.
97    pub const fn cuttable(mut self) -> Self {
98        self.cuttable = true;
99        self.copyable = true;
100        self
101    }
102    pub const fn selectable(mut self) -> Self {
103        self.selectable = true;
104        self
105    }
106    pub const fn themeable(mut self) -> Self {
107        self.themeable = true;
108        self
109    }
110    pub const fn resizable(mut self) -> Self {
111        self.resizable = true;
112        self
113    }
114    pub const fn searchable(mut self) -> Self {
115        self.searchable = true;
116        self
117    }
118    pub const fn editable(mut self) -> Self {
119        self.editable = true;
120        self
121    }
122    pub const fn navigable(mut self) -> Self {
123        self.navigable = true;
124        self
125    }
126    /// Its items can be dragged onto zones via [`crate::dragdrop`].
127    pub const fn draggable(mut self) -> Self {
128        self.draggable = true;
129        self
130    }
131    /// Its content lays on a shared [`crate::time_axis`] (pan/zoom-in-time).
132    pub const fn time_scrollable(mut self) -> Self {
133        self.time_scrollable = true;
134        self
135    }
136
137    /// JSON for the introspection/state dump.
138    pub fn to_json(self) -> serde_json::Value {
139        serde_json::json!({
140            "scalable": self.scalable, "copyable": self.copyable,
141            "pasteable": self.pasteable, "cuttable": self.cuttable,
142            "selectable": self.selectable, "themeable": self.themeable,
143            "resizable": self.resizable, "searchable": self.searchable,
144            "editable": self.editable, "navigable": self.navigable,
145            "draggable": self.draggable, "time_scrollable": self.time_scrollable,
146        })
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn none_is_all_false_and_equals_default() {
156        let n = FacetCaps::NONE;
157        assert!(!n.scalable && !n.copyable && !n.pasteable && !n.cuttable);
158        assert!(!n.selectable && !n.themeable && !n.resizable && !n.searchable);
159        assert!(!n.editable && !n.navigable && !n.draggable && !n.time_scrollable);
160        assert_eq!(FacetCaps::default(), FacetCaps::NONE);
161    }
162
163    #[test]
164    fn all_is_all_true() {
165        let a = FacetCaps::ALL;
166        assert!(a.scalable && a.copyable && a.pasteable && a.cuttable);
167        assert!(a.selectable && a.themeable && a.resizable && a.searchable);
168        assert!(a.editable && a.navigable && a.draggable && a.time_scrollable);
169    }
170
171    #[test]
172    fn builders_set_exactly_one_flag() {
173        assert_eq!(FacetCaps::NONE.scalable(), FacetCaps { scalable: true, ..FacetCaps::NONE });
174        assert_eq!(FacetCaps::NONE.selectable(), FacetCaps { selectable: true, ..FacetCaps::NONE });
175        assert_eq!(FacetCaps::NONE.searchable(), FacetCaps { searchable: true, ..FacetCaps::NONE });
176        assert_eq!(FacetCaps::NONE.editable(), FacetCaps { editable: true, ..FacetCaps::NONE });
177        assert_eq!(FacetCaps::NONE.draggable(), FacetCaps { draggable: true, ..FacetCaps::NONE });
178        assert_eq!(
179            FacetCaps::NONE.time_scrollable(),
180            FacetCaps { time_scrollable: true, ..FacetCaps::NONE }
181        );
182    }
183
184    #[test]
185    fn cuttable_implies_copyable() {
186        let c = FacetCaps::NONE.cuttable();
187        assert!(c.cuttable && c.copyable, "cut implies copy");
188    }
189
190    #[test]
191    fn builders_chain_in_const_context() {
192        const C: FacetCaps = FacetCaps::NONE.selectable().copyable().searchable().scalable().resizable().editable();
193        assert!(C.selectable && C.copyable && C.searchable && C.scalable && C.resizable && C.editable);
194        assert!(!C.pasteable && !C.cuttable && !C.themeable);
195    }
196
197    #[test]
198    fn to_json_carries_every_flag() {
199        let j = FacetCaps::NONE.scalable().themeable().editable().to_json();
200        assert_eq!(j["scalable"], true);
201        assert_eq!(j["themeable"], true);
202        assert_eq!(j["editable"], true);
203        assert_eq!(j["copyable"], false);
204        assert_eq!(j["navigable"], false);
205        assert_eq!(j["draggable"], false);
206        assert_eq!(j["time_scrollable"], false);
207        // all twelve keys present
208        assert_eq!(j.as_object().unwrap().len(), 12);
209    }
210}