Skip to main content

eui_core/
read_only.rs

1//! VAPP-200 (spec apps.md §9.2): a READ-ONLY surface (`src/read-only.ts`).
2//! A host whose viewer may read but not write (an app at app access
3//! `read`) sets [`crate::surface::SurfaceSettings::read_only`]; the build
4//! then gives every WRITING control `disabled: true`, as if its author had
5//! written it (the components' own disabled look, nothing new). Pure;
6//! `fixtures/read-only.json` locks it.
7//!
8//! A writing control: a node with an `on.<event>` action calling a write
9//! (`records.*`, `actions.*`; a Form whose submit writes disables its
10//! fields and buttons through the Form's cascade), or a form field with no
11//! action of its own. A node whose own actions are all pure or host calls
12//! (a `bind`, a `set`) is a view control and stays live.
13
14use serde_json::Value;
15
16use crate::types::UiNode;
17
18/// The components that edit a value ([`crate::layout_tree::FORM_FIELDS`]).
19pub const READ_ONLY_FIELDS: &[&str] = crate::layout_tree::FORM_FIELDS;
20
21/// The call prefixes that write at app access `read`.
22pub const READ_ONLY_WRITE_PREFIXES: &[&str] = &["records.", "actions."];
23
24/// Does this function name write (`records.*`, `actions.*`)?
25pub fn call_writes(name: &str) -> bool {
26    READ_ONLY_WRITE_PREFIXES.iter().any(|p| name.starts_with(p))
27}
28
29/// Does this action (`{functionCall: {call}}`) call a write?
30pub fn action_writes(action: &Value) -> bool {
31    action.get("functionCall").and_then(|f| f.get("call")).and_then(Value::as_str).is_some_and(call_writes)
32}
33
34/// Is `node` a writing control of a read-only surface?
35pub fn disables(component: &str, on: Option<&indexmap::IndexMap<String, Value>>) -> bool {
36    match on {
37        Some(on) if !on.is_empty() => on.values().any(action_writes),
38        _ => READ_ONLY_FIELDS.contains(&component),
39    }
40}
41
42fn already_disabled(node: &UiNode) -> bool {
43    node.props.get("disabled") == Some(&Value::Bool(true))
44}
45
46/// `node` with every writing control disabled (`props.disabled: true`), at
47/// any depth (slots and children). A host runs it over the root AND each
48/// lifted template; the surface does the same per node while it builds.
49pub fn read_only_tree(node: &UiNode) -> UiNode {
50    let mut out = node.clone();
51    apply(&mut out);
52    out
53}
54
55fn apply(node: &mut UiNode) {
56    if disables(&node.component, node.on.as_ref()) && !already_disabled(node) {
57        node.props.insert("disabled".into(), Value::Bool(true));
58    }
59    if let Some(slots) = node.slots.as_mut() {
60        slots.values_mut().for_each(apply);
61    }
62    node.children.iter_mut().for_each(apply);
63}
64
65/// The ids a read-only surface disables, pre-order (slots before children):
66/// the conformance check.
67pub fn disabled_ids(node: &UiNode) -> Vec<String> {
68    let mut out = Vec::new();
69    collect(node, &mut out);
70    out
71}
72
73fn collect(node: &UiNode, out: &mut Vec<String>) {
74    if disables(&node.component, node.on.as_ref()) && !already_disabled(node) {
75        out.push(node.id.clone());
76    }
77    for s in node.slots.iter().flat_map(|s| s.values()) {
78        collect(s, out);
79    }
80    for c in &node.children {
81        collect(c, out);
82    }
83}