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}