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::{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    node(
75        "box",
76        vec![
77            ("role", sym("structure")),
78            ("children", Expr::List(vec![tree_of("value", value)])),
79        ],
80    )
81}
82
83fn tree_of(label: &str, value: &Expr) -> Expr {
84    match value {
85        Expr::Map(entries) => node(
86            "tree",
87            vec![
88                ("label", Expr::String(label.to_owned())),
89                (
90                    "nodes",
91                    Expr::List(
92                        entries
93                            .iter()
94                            .map(|(key, child)| tree_of(&render_value(key), child))
95                            .collect(),
96                    ),
97                ),
98            ],
99        ),
100        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => node(
101            "tree",
102            vec![
103                ("label", Expr::String(format!("{label} [{}]", items.len()))),
104                (
105                    "nodes",
106                    Expr::List(
107                        items
108                            .iter()
109                            .enumerate()
110                            .map(|(index, child)| tree_of(&format!("[{index}]"), child))
111                            .collect(),
112                    ),
113                ),
114            ],
115        ),
116        atom => text_line(format!("{label}: {}", render_value(atom))),
117    }
118}
119
120/// Region 3: the canonical text. Each SCALAR leaf is an editable text field
121/// bound to its OWN field path, so committing an edit sets only that leaf and
122/// preserves its siblings (set semantics). A structured value is NOT exposed as
123/// a single root-path text field: text is not parsed back into structure here,
124/// so editing the whole value as text would clobber it. Structured editing is
125/// the structure tree's job; scalar leaves edit in place.
126fn canonical_text(value: &Expr) -> Expr {
127    let mut children = vec![text_line(render_value(value))];
128    match value {
129        Expr::Map(entries) => {
130            for (key, child) in entries {
131                if is_scalar(child) {
132                    children.push(editable_leaf(value, key_path(key), child));
133                }
134            }
135        }
136        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
137            for (index, item) in items.iter().enumerate() {
138                if is_scalar(item) {
139                    children.push(editable_leaf(value, index_path(index), item));
140                }
141            }
142        }
143        scalar => {
144            // A bare scalar IS its own leaf: editing it at the root path sets the
145            // whole (scalar) value, which is honest -- there is no structure to
146            // clobber.
147            children.push(editable_leaf(scalar, Expr::List(Vec::new()), scalar));
148        }
149    }
150    node(
151        "box",
152        vec![
153            ("role", sym("canonical-text")),
154            ("children", Expr::List(children)),
155        ],
156    )
157}
158
159/// True when `value` is an atom (no nested structure to edit per-field).
160fn is_scalar(value: &Expr) -> bool {
161    !matches!(
162        value,
163        Expr::Map(_) | Expr::List(_) | Expr::Vector(_) | Expr::Set(_)
164    )
165}
166
167/// The `k`/`i` wire path that scopes an edit to a single map key.
168fn key_path(key: &Expr) -> Expr {
169    Expr::List(vec![Expr::Vector(vec![sym("k"), key.clone()])])
170}
171
172/// The `k`/`i` wire path that scopes an edit to a single sequence index.
173fn index_path(index: usize) -> Expr {
174    Expr::List(vec![Expr::Vector(vec![
175        sym("i"),
176        Expr::String(index.to_string()),
177    ])])
178}
179
180/// An editable text field for one scalar `leaf`, bound to `path` within `root`.
181/// The field's `target` is the root value and `path` scopes the edit, so an
182/// `edit-field` built from it sets only that leaf.
183fn editable_leaf(root: &Expr, path: Expr, leaf: &Expr) -> Expr {
184    node(
185        "field",
186        vec![
187            ("kind", sym("text")),
188            ("value", Expr::String(render_value(leaf))),
189            ("target", root.clone()),
190            ("path", path),
191            ("readonly", Expr::Bool(false)),
192        ],
193    )
194}
195
196/// Region 4: properties and actions as buttons emitting `intent/invoke`.
197fn operations_inspector(value: &Expr) -> Expr {
198    node(
199        "stack",
200        vec![
201            ("role", sym("operations")),
202            ("dir", sym("column")),
203            (
204                "children",
205                Expr::List(vec![
206                    action_button("copy", "Copy", value),
207                    action_button("edit", "Edit", value),
208                ]),
209            ),
210        ],
211    )
212}
213
214fn action_button(control: &str, label: &str, value: &Expr) -> Expr {
215    node(
216        "button",
217        vec![
218            ("control", sym(control)),
219            ("label", Expr::String(label.to_owned())),
220            ("target", value.clone()),
221        ],
222    )
223}
224
225fn roundtrip_badge(value: &Expr) -> Expr {
226    let codec = CodecId(0);
227    match sim_codec::encode_portable(codec, value) {
228        Ok(text) => match sim_codec::decode_portable(codec, &text) {
229            Ok(decoded) if &decoded == value => badge("ok", "round-trips"),
230            Ok(_) => badge("warn", "round-trip differs"),
231            Err(_) => badge("warn", "decode failed"),
232        },
233        Err(_) => badge("info", "non-data value"),
234    }
235}
236
237/// The four universal regions in increasing-depth order: summary, canonical
238/// text, structure tree, operations. Mode-aware rendering (P9) takes a prefix.
239pub(crate) fn universal_regions(value: &Expr) -> Vec<Expr> {
240    vec![
241        summary_card(value),
242        canonical_text(value),
243        structure_tree(value),
244        operations_inspector(value),
245    ]
246}
247
248/// A short human-readable kind name for the value.
249pub use sim_value::kind::expr_kind;
250
251fn short_label(value: &Expr) -> String {
252    let rendered = render_value(value);
253    if rendered.len() <= 48 {
254        rendered
255    } else {
256        format!("{}...", &rendered[..45])
257    }
258}
259
260/// Render a value as compact, readable text for display.
261pub fn render_value(value: &Expr) -> String {
262    match value {
263        Expr::Nil => "nil".to_owned(),
264        Expr::Bool(flag) => flag.to_string(),
265        Expr::Number(number) => number.canonical.clone(),
266        Expr::Symbol(symbol) | Expr::Local(symbol) => symbol.as_qualified_str(),
267        Expr::String(text) => format!("{text:?}"),
268        Expr::Bytes(bytes) => format!("#bytes({})", bytes.len()),
269        Expr::List(items) => format!("({})", render_items(items)),
270        Expr::Vector(items) => format!("[{}]", render_items(items)),
271        Expr::Set(items) => format!("#{{{}}}", render_items(items)),
272        Expr::Map(entries) => {
273            let body = entries
274                .iter()
275                .map(|(key, value)| format!("{}: {}", render_value(key), render_value(value)))
276                .collect::<Vec<_>>()
277                .join(", ");
278            format!("{{{body}}}")
279        }
280        other => format!("<{}>", expr_kind(other)),
281    }
282}
283
284fn render_items(items: &[Expr]) -> String {
285    items.iter().map(render_value).collect::<Vec<_>>().join(" ")
286}