Skip to main content

sim_lib_view/
mode.rs

1//! Experience modes and capability-aware action exposure.
2//!
3//! One runtime serves five audiences without forking into five products.
4//! Audience differences are expressed as **modes** -- Household, Builder,
5//! Systems -- that change which lenses, controls, and verbosity are shown, gated
6//! by capability and role. Modes never change the underlying value: the same
7//! value renders at different depth, but it is the same value.
8
9use sim_kernel::{CapabilityName, Expr, Symbol};
10use sim_lib_scene::node;
11
12use crate::universal_view::universal_regions;
13
14/// An experience mode. Modes change depth and control exposure, not the value.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Mode {
17    /// Large, safe, jargon-free controls for non-technical users.
18    Household,
19    /// The default working depth for regular users and coders.
20    Builder,
21    /// Full depth -- structure, operations, raw -- for developers and admins.
22    Systems,
23}
24
25impl Mode {
26    /// Parse a mode symbol name.
27    pub fn from_name(name: &str) -> Option<Self> {
28        match name {
29            "household" => Some(Mode::Household),
30            "builder" => Some(Mode::Builder),
31            "systems" => Some(Mode::Systems),
32            _ => None,
33        }
34    }
35
36    /// The mode's symbol.
37    pub fn symbol(self) -> Symbol {
38        Symbol::new(match self {
39            Mode::Household => "household",
40            Mode::Builder => "builder",
41            Mode::Systems => "systems",
42        })
43    }
44
45    /// How many universal regions this mode shows (its depth).
46    pub fn depth(self) -> usize {
47        match self {
48            Mode::Household => 2,
49            Mode::Builder => 3,
50            Mode::Systems => 4,
51        }
52    }
53}
54
55/// Render a value through the universal default lens at the depth of `mode`.
56/// Household shows a friendly summary and the canonical text; Builder adds the
57/// structure tree; Systems adds the operations inspector. The value is never
58/// changed.
59pub fn universal_scene(value: &Expr, mode: Mode) -> Expr {
60    let mut regions = universal_regions(value);
61    regions.truncate(mode.depth());
62    node(
63        "stack",
64        vec![
65            ("id", Expr::Symbol(Symbol::new("universal"))),
66            ("dir", Expr::Symbol(Symbol::new("column"))),
67            ("mode", Expr::Symbol(mode.symbol())),
68            ("children", Expr::List(regions)),
69        ],
70    )
71}
72
73/// Whether and how an action is exposed.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum Exposure {
76    /// Shown and directly actionable.
77    Shown,
78    /// Shown but requires a confirmation overlay carrying the exact operation.
79    ConfirmationGated,
80    /// Absent entirely (not disabled-and-tantalizing).
81    Absent,
82}
83
84/// Decide how to expose an action, given the capabilities it requires, the
85/// granted set, whether it is dangerous, and the active mode.
86///
87/// A missing capability makes the action absent (admin actions do not appear
88/// disabled). A dangerous action is confirmation-gated when capable, and absent
89/// in Household mode. Everything else is shown.
90pub fn action_exposure(
91    required: &[CapabilityName],
92    granted: impl Fn(&CapabilityName) -> bool,
93    dangerous: bool,
94    mode: Mode,
95) -> Exposure {
96    if !required.iter().all(&granted) {
97        return Exposure::Absent;
98    }
99    if dangerous {
100        return match mode {
101            Mode::Household => Exposure::Absent,
102            _ => Exposure::ConfirmationGated,
103        };
104    }
105    Exposure::Shown
106}
107
108/// A clear "action denied" Scene -- never a blank dead end.
109pub fn denied_scene(reason: &str) -> Expr {
110    node(
111        "box",
112        vec![
113            ("role", Expr::Symbol(Symbol::new("denied"))),
114            (
115                "children",
116                Expr::List(vec![
117                    node(
118                        "badge",
119                        vec![
120                            ("status", Expr::Symbol(Symbol::new("error"))),
121                            ("label", Expr::String("denied".to_owned())),
122                        ],
123                    ),
124                    node("text", vec![("text", Expr::String(reason.to_owned()))]),
125                ]),
126            ),
127        ],
128    )
129}
130
131/// A read-only rendering: the value at the mode's depth, clearly marked
132/// read-only, with no committing controls.
133pub fn readonly_scene(value: &Expr, mode: Mode) -> Expr {
134    node(
135        "stack",
136        vec![
137            ("role", Expr::Symbol(Symbol::new("readonly"))),
138            ("dir", Expr::Symbol(Symbol::new("column"))),
139            (
140                "children",
141                Expr::List(vec![
142                    node(
143                        "badge",
144                        vec![
145                            ("status", Expr::Symbol(Symbol::new("info"))),
146                            ("label", Expr::String("read-only".to_owned())),
147                        ],
148                    ),
149                    universal_scene(value, mode),
150                ]),
151            ),
152        ],
153    )
154}