facett-core/src/caps.rs
//! **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,
/// Drives the shared [`Navigable`](crate::nav::Navigable) pan/zoom/fit model
/// (wheel zoom-to-cursor, drag-pan, keyboard pan/zoom, fit-to-view). A host
/// can offer navigation help / a fit affordance when this is set.
pub navigable: bool,
/// Its items can be **dragged and dropped** onto zones (cards→columns,
/// bars→lanes) via the shared [`dragdrop`](crate::dragdrop) primitive. A host
/// can offer drag affordances / a "moves pending" indicator when set.
pub draggable: bool,
/// Lays its content on a **shared time axis** ([`time_axis`](crate::time_axis))
/// that can be panned/zoomed in time (gantt, CFD, calendar, timeline). A host
/// can link several such facets to one axis and offer a time-scrubber.
pub time_scrollable: 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,
navigable: false,
draggable: false,
time_scrollable: 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,
navigable: true,
draggable: true,
time_scrollable: 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
}
pub const fn navigable(mut self) -> Self {
self.navigable = true;
self
}
/// Its items can be dragged onto zones via [`crate::dragdrop`].
pub const fn draggable(mut self) -> Self {
self.draggable = true;
self
}
/// Its content lays on a shared [`crate::time_axis`] (pan/zoom-in-time).
pub const fn time_scrollable(mut self) -> Self {
self.time_scrollable = 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, "navigable": self.navigable,
"draggable": self.draggable, "time_scrollable": self.time_scrollable,
})
}
}
#[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 && !n.navigable && !n.draggable && !n.time_scrollable);
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 && a.navigable && a.draggable && a.time_scrollable);
}
#[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 });
assert_eq!(FacetCaps::NONE.draggable(), FacetCaps { draggable: true, ..FacetCaps::NONE });
assert_eq!(
FacetCaps::NONE.time_scrollable(),
FacetCaps { time_scrollable: 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();
const _: () = assert!(C.selectable && C.copyable && C.searchable && C.scalable && C.resizable && C.editable);
const _: () = 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);
assert_eq!(j["navigable"], false);
assert_eq!(j["draggable"], false);
assert_eq!(j["time_scrollable"], false);
// all twelve keys present
assert_eq!(j.as_object().unwrap().len(), 12);
}
}