hypen_engine/reconcile/
tree.rs

1use crate::ir::{Element, NodeId, Value};
2use indexmap::IndexMap;
3use slotmap::SlotMap;
4
5/// Instance node - a concrete instance of an element in the tree
6#[derive(Debug, Clone)]
7pub struct InstanceNode {
8    /// Unique node ID
9    pub id: NodeId,
10
11    /// Element type (e.g., "Column", "Text")
12    pub element_type: String,
13
14    /// Resolved props (bindings evaluated to actual values)
15    pub props: IndexMap<String, serde_json::Value>,
16
17    /// Raw props (including bindings) for change detection
18    pub raw_props: IndexMap<String, Value>,
19
20    /// Original element template (for List re-rendering)
21    /// Only populated for List elements that need to re-render children
22    pub element_template: Option<Element>,
23
24    // Event handling removed - now done at renderer level
25
26    /// Optional key for reconciliation
27    pub key: Option<String>,
28
29    /// Parent node ID
30    pub parent: Option<NodeId>,
31
32    /// Child node IDs (ordered)
33    pub children: Vec<NodeId>,
34}
35
36impl InstanceNode {
37    pub fn new(id: NodeId, element: &Element, state: &serde_json::Value) -> Self {
38        let props = resolve_props(&element.props, state);
39
40        Self {
41            id,
42            element_type: element.element_type.clone(),
43            props,
44            raw_props: element.props.clone(),
45            element_template: None,
46            key: element.key.clone(),
47            parent: None,
48            children: Vec::new(),
49        }
50    }
51
52    /// Update props by re-evaluating bindings against new state
53    pub fn update_props(&mut self, state: &serde_json::Value) {
54        self.props = resolve_props(&self.raw_props, state);
55    }
56}
57
58/// The instance tree - maintains the current UI tree state
59pub struct InstanceTree {
60    /// All nodes in the tree
61    nodes: SlotMap<NodeId, InstanceNode>,
62
63    /// Root node ID
64    root: Option<NodeId>,
65}
66
67impl InstanceTree {
68    pub fn new() -> Self {
69        Self {
70            nodes: SlotMap::with_key(),
71            root: None,
72        }
73    }
74
75    /// Clear all nodes and reset the tree
76    pub fn clear(&mut self) {
77        self.nodes.clear();
78        self.root = None;
79    }
80
81    /// Create a new node and return its ID
82    pub fn create_node(&mut self, element: &Element, state: &serde_json::Value) -> NodeId {
83        let id = self.nodes.insert_with_key(|id| InstanceNode::new(id, element, state));
84        id
85    }
86
87    /// Get a node by ID
88    pub fn get(&self, id: NodeId) -> Option<&InstanceNode> {
89        self.nodes.get(id)
90    }
91
92    /// Get a mutable node by ID
93    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut InstanceNode> {
94        self.nodes.get_mut(id)
95    }
96
97    /// Remove a node and all its descendants
98    pub fn remove(&mut self, id: NodeId) -> Option<InstanceNode> {
99        if let Some(node) = self.nodes.get(id) {
100            let children = node.children.clone();
101            // Remove all children recursively
102            for child_id in children {
103                self.remove(child_id);
104            }
105        }
106        self.nodes.remove(id)
107    }
108
109    /// Set the root node
110    pub fn set_root(&mut self, id: NodeId) {
111        self.root = Some(id);
112    }
113
114    /// Get the root node ID
115    pub fn root(&self) -> Option<NodeId> {
116        self.root
117    }
118
119    /// Add a child to a parent node
120    pub fn add_child(&mut self, parent_id: NodeId, child_id: NodeId, before: Option<NodeId>) {
121        if let Some(parent) = self.nodes.get_mut(parent_id) {
122            if let Some(before_id) = before {
123                if let Some(pos) = parent.children.iter().position(|&id| id == before_id) {
124                    parent.children.insert(pos, child_id);
125                } else {
126                    parent.children.push(child_id);
127                }
128            } else {
129                parent.children.push(child_id);
130            }
131        }
132
133        if let Some(child) = self.nodes.get_mut(child_id) {
134            child.parent = Some(parent_id);
135        }
136    }
137
138    /// Remove a child from its parent
139    pub fn remove_child(&mut self, parent_id: NodeId, child_id: NodeId) {
140        if let Some(parent) = self.nodes.get_mut(parent_id) {
141            parent.children.retain(|&id| id != child_id);
142        }
143
144        if let Some(child) = self.nodes.get_mut(child_id) {
145            child.parent = None;
146        }
147    }
148
149    /// Update all nodes that depend on changed state
150    pub fn update_nodes(&mut self, node_ids: &indexmap::IndexSet<NodeId>, state: &serde_json::Value) {
151        for &node_id in node_ids {
152            if let Some(node) = self.nodes.get_mut(node_id) {
153                node.update_props(state);
154            }
155        }
156    }
157
158    /// Iterate over all nodes
159    pub fn iter(&self) -> impl Iterator<Item = (NodeId, &InstanceNode)> {
160        self.nodes.iter()
161    }
162}
163
164impl Default for InstanceTree {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170/// Resolve props by evaluating bindings against current state
171fn resolve_props(props: &IndexMap<String, Value>, state: &serde_json::Value) -> IndexMap<String, serde_json::Value> {
172    let mut resolved = IndexMap::new();
173
174    for (key, value) in props {
175        let resolved_value = match value {
176            Value::Static(v) => v.clone(),
177            Value::Binding(binding) => {
178                // Evaluate binding against state using the parsed path
179                evaluate_binding(binding, state).unwrap_or(serde_json::Value::Null)
180            }
181            Value::TemplateString { template, bindings } => {
182                // Interpolate all bindings in the template string
183                let mut result = template.clone();
184                for binding in bindings {
185                    let binding_placeholder = format!("${{state.{}}}", binding.full_path());
186                    let value = evaluate_binding(binding, state)
187                        .map(|v| match v {
188                            serde_json::Value::String(s) => s,
189                            other => other.to_string(),
190                        })
191                        .unwrap_or_default();
192                    result = result.replace(&binding_placeholder, &value);
193                }
194                serde_json::Value::String(result)
195            }
196            Value::Action(action) => {
197                // Actions are serialized with @ prefix for renderer to detect
198                serde_json::Value::String(format!("@{}", action))
199            }
200        };
201        resolved.insert(key.clone(), resolved_value);
202    }
203
204    resolved
205}
206
207/// Evaluate a binding against the state object using its parsed path
208fn evaluate_binding(binding: &crate::reactive::Binding, state: &serde_json::Value) -> Option<serde_json::Value> {
209    let mut current = state;
210
211    for segment in &binding.path {
212        current = current.get(segment)?;
213    }
214
215    Some(current.clone())
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use serde_json::json;
222
223    #[test]
224    fn test_evaluate_binding() {
225        use crate::reactive::Binding;
226
227        let state = json!({
228            "user": {
229                "name": "Alice",
230                "age": 30
231            }
232        });
233
234        let name_binding = Binding::new(vec!["user".to_string(), "name".to_string()]);
235        let age_binding = Binding::new(vec!["user".to_string(), "age".to_string()]);
236        let email_binding = Binding::new(vec!["user".to_string(), "email".to_string()]);
237
238        assert_eq!(
239            evaluate_binding(&name_binding, &state),
240            Some(json!("Alice"))
241        );
242        assert_eq!(
243            evaluate_binding(&age_binding, &state),
244            Some(json!(30))
245        );
246        assert_eq!(evaluate_binding(&email_binding, &state), None);
247    }
248}