Skip to main content

sim_lib_view/
universal_view.rs

1//! The universal default view: a complete Scene for any value with no
2//! specialized lens.
3//!
4//! Every value must open even when nothing specialized claims it. This view
5//! emits a four-region Scene -- a summary card, a structure tree, the canonical
6//! text, and an operations inspector -- built only from baseline scene node
7//! kinds, so it is shipped and polished rather than a stub.
8
9use sim_kernel::{CodecId, Cx, Expr, Result};
10use sim_lib_scene::{SceneBudget, SceneBudgetExhausted, SceneBudgetState, node, sym};
11
12use crate::contract::View;
13
14/// The universal default view object.
15pub struct UniversalView;
16
17impl View for UniversalView {
18    fn encode(&self, _cx: &mut Cx, value: &Expr) -> Result<Expr> {
19        Ok(node(
20            "stack",
21            vec![
22                ("id", sym("universal")),
23                ("dir", sym("column")),
24                (
25                    "children",
26                    Expr::List(vec![
27                        summary_card(value),
28                        structure_tree(value),
29                        canonical_text(value),
30                        operations_inspector(value),
31                    ]),
32                ),
33            ],
34        ))
35    }
36}
37
38fn text_line(text: String) -> Expr {
39    node("text", vec![("text", Expr::String(text))])
40}
41
42fn badge(status: &str, label: &str) -> Expr {
43    // Status carries a text token; it never relies on color alone.
44    node(
45        "badge",
46        vec![
47            ("status", sym(status)),
48            ("label", Expr::String(label.to_owned())),
49        ],
50    )
51}
52
53/// Region 1: class/identity/kind/round-trip summary.
54fn summary_card(value: &Expr) -> Expr {
55    let roundtrip = roundtrip_badge(value);
56    node(
57        "box",
58        vec![
59            ("role", sym("summary")),
60            (
61                "children",
62                Expr::List(vec![
63                    text_line(format!("kind: {}", expr_kind(value))),
64                    text_line(format!("label: {}", short_label(value))),
65                    roundtrip,
66                ]),
67            ),
68        ],
69    )
70}
71
72/// Region 2: an expandable structure tree.
73fn structure_tree(value: &Expr) -> Expr {
74    let mut budget = SceneBudgetState::new(SceneBudget::interactive());
75    node(
76        "box",
77        vec![
78            ("role", sym("structure")),
79            (
80                "children",
81                Expr::List(vec![tree_of("value", value, &mut budget, 0, Vec::new())]),
82            ),
83        ],
84    )
85}
86
87fn tree_of(
88    label: &str,
89    value: &Expr,
90    budget: &mut SceneBudgetState,
91    depth: usize,
92    path: Vec<Expr>,
93) -> Expr {
94    let encoded_bytes = estimated_tree_bytes(label, value);
95    if let Err(exhausted) = budget.admit(depth, Some(label), encoded_bytes) {
96        return continuation_node(label, exhausted);
97    }
98    match value {
99        Expr::Map(entries) => node(
100            "tree",
101            vec![
102                ("label", Expr::String(label.to_owned())),
103                ("open", Expr::Bool(depth == 0)),
104                ("aria-expanded", Expr::Bool(depth == 0)),
105                ("disclosure-target", Expr::List(path.clone())),
106                (
107                    "nodes",
108                    Expr::List(
109                        entries
110                            .iter()
111                            .map(|(key, child)| {
112                                let mut child_path = path.clone();
113                                child_path.push(Expr::Vector(vec![sym("k"), key.clone()]));
114                                tree_of(&render_value(key), child, budget, depth + 1, child_path)
115                            })
116                            .collect(),
117                    ),
118                ),
119            ],
120        ),
121        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => node(
122            "tree",
123            vec![
124                ("label", Expr::String(format!("{label} [{}]", items.len()))),
125                ("open", Expr::Bool(depth == 0)),
126                ("aria-expanded", Expr::Bool(depth == 0)),
127                ("disclosure-target", Expr::List(path.clone())),
128                (
129                    "nodes",
130                    Expr::List(
131                        items
132                            .iter()
133                            .enumerate()
134                            .map(|(index, child)| {
135                                let mut child_path = path.clone();
136                                child_path.push(Expr::Vector(vec![
137                                    sym("i"),
138                                    Expr::String(index.to_string()),
139                                ]));
140                                tree_of(&format!("[{index}]"), child, budget, depth + 1, child_path)
141                            })
142                            .collect(),
143                    ),
144                ),
145            ],
146        ),
147        atom => text_line(format!("{label}: {}", render_value(atom))),
148    }
149}
150
151fn continuation_node(label: &str, exhausted: SceneBudgetExhausted) -> Expr {
152    node(
153        "continuation",
154        vec![
155            ("label", Expr::String(format!("{label}: more not rendered"))),
156            ("truncated", Expr::Bool(true)),
157            ("reason", sym(exhausted.reason())),
158            ("limit", Expr::String(exhausted.limit().to_string())),
159        ],
160    )
161}
162
163/// Region 3: the canonical text. Each SCALAR leaf is an editable text field
164/// bound to its OWN field path, so committing an edit sets only that leaf and
165/// preserves its siblings (set semantics). A structured value is NOT exposed as
166/// a single root-path text field: text is not parsed back into structure here,
167/// so editing the whole value as text would clobber it. Structured editing is
168/// the structure tree's job; scalar leaves edit in place.
169fn canonical_text(value: &Expr) -> Expr {
170    let mut children = vec![text_line(render_value(value))];
171    match value {
172        Expr::Map(entries) => {
173            for (key, child) in entries {
174                if is_scalar(child) {
175                    children.push(editable_leaf(value, key_path(key), child));
176                }
177            }
178        }
179        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
180            for (index, item) in items.iter().enumerate() {
181                if is_scalar(item) {
182                    children.push(editable_leaf(value, index_path(index), item));
183                }
184            }
185        }
186        scalar => {
187            // A bare scalar IS its own leaf: editing it at the root path sets the
188            // whole (scalar) value, which is honest -- there is no structure to
189            // clobber.
190            children.push(editable_leaf(scalar, Expr::List(Vec::new()), scalar));
191        }
192    }
193    node(
194        "box",
195        vec![
196            ("role", sym("canonical-text")),
197            ("children", Expr::List(children)),
198        ],
199    )
200}
201
202/// True when `value` is an atom (no nested structure to edit per-field).
203fn is_scalar(value: &Expr) -> bool {
204    !matches!(
205        value,
206        Expr::Map(_) | Expr::List(_) | Expr::Vector(_) | Expr::Set(_)
207    )
208}
209
210/// The `k`/`i` wire path that scopes an edit to a single map key.
211fn key_path(key: &Expr) -> Expr {
212    Expr::List(vec![Expr::Vector(vec![sym("k"), key.clone()])])
213}
214
215/// The `k`/`i` wire path that scopes an edit to a single sequence index.
216fn index_path(index: usize) -> Expr {
217    Expr::List(vec![Expr::Vector(vec![
218        sym("i"),
219        Expr::String(index.to_string()),
220    ])])
221}
222
223/// An editable text field for one scalar `leaf`, bound to `path` within `root`.
224/// The field's `target` is the root value and `path` scopes the edit, so an
225/// `edit-field` built from it sets only that leaf.
226fn editable_leaf(root: &Expr, path: Expr, leaf: &Expr) -> Expr {
227    let mut fields = vec![
228        ("input-kind", sym("text")),
229        ("value", Expr::String(render_value(leaf))),
230        ("value-kind", sym(expr_kind(leaf))),
231        ("target", root.clone()),
232        ("path", path),
233        ("readonly", Expr::Bool(false)),
234    ];
235    if let Ok(encoded) = sim_codec::encode_portable(CodecId(0), leaf) {
236        fields.push(("value-codec", Expr::String(encoded)));
237    }
238    node("field", fields)
239}
240
241/// Region 4: properties and actions as buttons emitting `intent/invoke`.
242fn operations_inspector(value: &Expr) -> Expr {
243    node(
244        "stack",
245        vec![
246            ("role", sym("operations")),
247            ("dir", sym("column")),
248            (
249                "children",
250                Expr::List(vec![
251                    action_button("copy", "Copy", value),
252                    action_button("edit", "Edit", value),
253                ]),
254            ),
255        ],
256    )
257}
258
259fn action_button(control: &str, label: &str, value: &Expr) -> Expr {
260    node(
261        "button",
262        vec![
263            ("control", sym(control)),
264            ("label", Expr::String(label.to_owned())),
265            ("target", value.clone()),
266        ],
267    )
268}
269
270fn roundtrip_badge(value: &Expr) -> Expr {
271    if exceeds_depth(value, 64) {
272        return badge("info", "too deep to round-trip inline");
273    }
274    let codec = CodecId(0);
275    match sim_codec::encode_portable(codec, value) {
276        Ok(text) => match sim_codec::decode_portable(codec, &text) {
277            Ok(decoded) if &decoded == value => badge("ok", "round-trips"),
278            Ok(_) => badge("warn", "round-trip differs"),
279            Err(_) => badge("warn", "decode failed"),
280        },
281        Err(_) => badge("info", "non-data value"),
282    }
283}
284
285fn exceeds_depth(value: &Expr, max_depth: usize) -> bool {
286    let mut stack = vec![(value, 0usize)];
287    while let Some((value, depth)) = stack.pop() {
288        if depth > max_depth {
289            return true;
290        }
291        match value {
292            Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
293                stack.extend(items.iter().map(|item| (item, depth + 1)));
294            }
295            Expr::Map(entries) => {
296                for (key, value) in entries {
297                    stack.push((key, depth + 1));
298                    stack.push((value, depth + 1));
299                }
300            }
301            _ => {}
302        }
303    }
304    false
305}
306
307/// The four universal regions in increasing-depth order: summary, canonical
308/// text, structure tree, operations. Mode-aware rendering takes a prefix.
309pub(crate) fn universal_regions(value: &Expr) -> Vec<Expr> {
310    vec![
311        summary_card(value),
312        canonical_text(value),
313        structure_tree(value),
314        operations_inspector(value),
315    ]
316}
317
318/// A short human-readable kind name for the value.
319pub use sim_value::kind::expr_kind;
320
321fn short_label(value: &Expr) -> String {
322    let rendered = render_value(value);
323    if rendered.len() <= 48 {
324        rendered
325    } else {
326        format!("{}...", &rendered[..45])
327    }
328}
329
330/// Render a value as compact, readable text for display.
331pub fn render_value(value: &Expr) -> String {
332    render_value_bounded(value, 0)
333}
334
335fn render_value_bounded(value: &Expr, depth: usize) -> String {
336    if depth >= 64 {
337        return "...".to_owned();
338    }
339    match value {
340        Expr::Nil => "nil".to_owned(),
341        Expr::Bool(flag) => flag.to_string(),
342        Expr::Number(number) => number.canonical.clone(),
343        Expr::Symbol(symbol) | Expr::Local(symbol) => symbol.as_qualified_str(),
344        Expr::String(text) => format!("{text:?}"),
345        Expr::Bytes(bytes) => format!("#bytes({})", bytes.len()),
346        Expr::List(items) => format!("({})", render_items(items, depth + 1)),
347        Expr::Vector(items) => format!("[{}]", render_items(items, depth + 1)),
348        Expr::Set(items) => format!("#{{{}}}", render_items(items, depth + 1)),
349        Expr::Map(entries) => {
350            let body = entries
351                .iter()
352                .take(128)
353                .map(|(key, value)| {
354                    format!(
355                        "{}: {}",
356                        render_value_bounded(key, depth + 1),
357                        render_value_bounded(value, depth + 1)
358                    )
359                })
360                .collect::<Vec<_>>()
361                .join(", ");
362            if entries.len() > 128 {
363                format!("{{{body}, ...}}")
364            } else {
365                format!("{{{body}}}")
366            }
367        }
368        other => format!("<{}>", expr_kind(other)),
369    }
370}
371
372fn render_items(items: &[Expr], depth: usize) -> String {
373    let rendered = items
374        .iter()
375        .take(128)
376        .map(|item| render_value_bounded(item, depth + 1))
377        .collect::<Vec<_>>()
378        .join(" ");
379    if items.len() > 128 {
380        format!("{rendered} ...")
381    } else {
382        rendered
383    }
384}
385
386fn estimated_tree_bytes(label: &str, value: &Expr) -> usize {
387    let body = match value {
388        Expr::Map(entries) => entries.len().saturating_mul(16),
389        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => items.len().saturating_mul(8),
390        atom => render_value(atom).len(),
391    };
392    label.len().saturating_add(body)
393}