hypen_engine/
render.rs

1//! Shared rendering logic for Engine and WasmEngine
2//!
3//! This module contains the common rendering logic to avoid code duplication
4//! between the native Engine and WASM WasmEngine implementations.
5
6use crate::{
7    ir::{NodeId, Value},
8    lifecycle::ModuleInstance,
9    reconcile::{InstanceTree, Patch},
10    reactive::{DependencyGraph, Scheduler},
11};
12
13/// Render only dirty nodes (optimized for state changes)
14/// This is shared logic used by both Engine and WasmEngine
15pub fn render_dirty_nodes(
16    scheduler: &mut Scheduler,
17    tree: &mut InstanceTree,
18    module: Option<&ModuleInstance>,
19) -> Vec<Patch> {
20    render_dirty_nodes_with_deps(scheduler, tree, module, &mut DependencyGraph::new())
21}
22
23/// Render only dirty nodes with dependency tracking for List reconciliation
24pub fn render_dirty_nodes_with_deps(
25    scheduler: &mut Scheduler,
26    tree: &mut InstanceTree,
27    module: Option<&ModuleInstance>,
28    dependencies: &mut DependencyGraph,
29) -> Vec<Patch> {
30    if !scheduler.has_dirty() {
31        return Vec::new();
32    }
33
34    let dirty_nodes = scheduler.take_dirty();
35    let state = module
36        .map(|m| m.get_state())
37        .unwrap_or(&serde_json::Value::Null);
38
39    // Store old props before updating, then update and generate patches for changed props only
40    let mut patches = Vec::new();
41    for node_id in dirty_nodes {
42        // Check if this is a List node (has array binding in raw_props AND element_template)
43        // The element_template is set for List elements that need to re-render children
44        let is_list_node = tree.get(node_id)
45            .map(|n| {
46                n.raw_props.get("0").map(|v| matches!(v, Value::Binding(_))).unwrap_or(false)
47                    && n.element_template.is_some()
48            })
49            .unwrap_or(false);
50
51        if is_list_node {
52            // For List nodes, we need to re-reconcile the entire list
53            render_dirty_list(node_id, tree, state, &mut patches, dependencies);
54        } else {
55            // Regular node: just update props
56            let old_props = tree.get(node_id).map(|n| n.props.clone());
57
58            // Update props with new state
59            if let Some(node) = tree.get_mut(node_id) {
60                node.update_props(state);
61            }
62
63            // Compare and generate patches only for changed props
64            if let (Some(old), Some(node)) = (old_props, tree.get(node_id)) {
65                for (key, new_value) in &node.props {
66                    // Only patch if the prop changed
67                    if old.get(key) != Some(new_value) {
68                        patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
69                    }
70                }
71            }
72        }
73    }
74
75    patches
76}
77
78/// Render a dirty List node by re-reconciling its children
79fn render_dirty_list(
80    node_id: NodeId,
81    tree: &mut InstanceTree,
82    state: &serde_json::Value,
83    patches: &mut Vec<Patch>,
84    dependencies: &mut DependencyGraph,
85) {
86    use crate::reconcile::diff::{create_tree, replace_item_bindings};
87
88    // Get the array binding, element template, and old children
89    let (array_binding, element_template, old_children) = {
90        let node = match tree.get(node_id) {
91            Some(n) => n,
92            None => return,
93        };
94
95        let binding = match node.raw_props.get("0") {
96            Some(Value::Binding(b)) => b.clone(),
97            _ => return,
98        };
99
100        let template = match &node.element_template {
101            Some(t) => t.clone(),
102            None => return, // No template stored, can't re-render
103        };
104
105        (binding, template, node.children.clone())
106    };
107
108    // Evaluate the array binding
109    let array = evaluate_binding(&array_binding, state)
110        .unwrap_or(serde_json::Value::Array(vec![]));
111
112    let items = match &array {
113        serde_json::Value::Array(items) => items,
114        _ => return,
115    };
116
117    // Remove all old children first
118    for &child_id in &old_children {
119        patches.push(Patch::remove(child_id));
120        tree.remove(child_id);
121    }
122
123    // Clear children from node
124    if let Some(node) = tree.get_mut(node_id) {
125        node.children.clear();
126    }
127
128    // Create new children for each item in the array
129    for (index, item) in items.iter().enumerate() {
130        for child_template in &element_template.children {
131            // Clone child and replace ${item.x} bindings with actual item data
132            let child_with_item = replace_item_bindings(child_template, item, index);
133            create_tree(tree, &child_with_item, Some(node_id), state, patches, false, dependencies);
134        }
135    }
136}
137
138/// Evaluate a binding against state
139fn evaluate_binding(binding: &crate::reactive::Binding, state: &serde_json::Value) -> Option<serde_json::Value> {
140    let mut current = state;
141    for segment in &binding.path {
142        current = current.get(segment)?;
143    }
144    Some(current.clone())
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::{ir::Element, lifecycle::Module, reactive::Binding};
151    use serde_json::json;
152
153    #[test]
154    fn test_render_dirty_nodes_no_dirty() {
155        let mut scheduler = Scheduler::new();
156        let mut tree = InstanceTree::new();
157
158        let patches = render_dirty_nodes(&mut scheduler, &mut tree, None);
159        assert_eq!(patches.len(), 0);
160    }
161
162    #[test]
163    fn test_render_dirty_nodes_with_changes() {
164        let mut scheduler = Scheduler::new();
165        let mut tree = InstanceTree::new();
166
167        // Create a module with state
168        let module = Module::new("TestModule");
169        let initial_state = json!({"count": 0});
170        let instance = ModuleInstance::new(module, initial_state);
171
172        // Create a node with a binding
173        let element = Element::new("Text");
174        let node_id = tree.create_node(&element, instance.get_state());
175
176        // Mark the node as dirty
177        scheduler.mark_dirty(node_id);
178
179        // Render dirty nodes
180        let patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
181
182        // Should have processed the dirty node (though patches may be empty if props didn't change)
183        assert!(!scheduler.has_dirty(), "Scheduler should have no more dirty nodes");
184    }
185
186    #[test]
187    fn test_render_dirty_nodes_state_change() {
188        use crate::ir::Value;
189
190        let mut scheduler = Scheduler::new();
191        let mut tree = InstanceTree::new();
192
193        // Create module with initial state
194        let module = Module::new("TestModule");
195        let initial_state = json!({"text": "Hello"});
196        let mut instance = ModuleInstance::new(module, initial_state);
197
198        // Create a node with a binding to state
199        let mut element = Element::new("Text");
200        element
201            .props
202            .insert("0".to_string(), Value::Binding(Binding::state(vec!["text".to_string()])));
203        let node_id = tree.create_node(&element, instance.get_state());
204
205        // Update the node's props to reflect initial state
206        if let Some(node) = tree.get_mut(node_id) {
207            node.update_props(instance.get_state());
208        }
209
210        // Update state
211        instance.update_state(json!({"text": "World"}));
212
213        // Mark node as dirty
214        scheduler.mark_dirty(node_id);
215
216        // Render dirty nodes
217        let patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
218
219        // Should generate a SetProp patch for the changed text
220        let set_prop_count = patches
221            .iter()
222            .filter(|p| matches!(p, Patch::SetProp { .. }))
223            .count();
224        assert!(set_prop_count > 0, "Should have SetProp patches for changed state");
225    }
226
227    #[test]
228    fn test_render_dirty_nodes_multiple_nodes() {
229        let mut scheduler = Scheduler::new();
230        let mut tree = InstanceTree::new();
231
232        let module = Module::new("TestModule");
233        let initial_state = json!({});
234        let instance = ModuleInstance::new(module, initial_state);
235
236        // Create multiple nodes
237        let element1 = Element::new("Text");
238        let element2 = Element::new("Text");
239        let element3 = Element::new("Text");
240
241        let node_id_1 = tree.create_node(&element1, instance.get_state());
242        let node_id_2 = tree.create_node(&element2, instance.get_state());
243        let node_id_3 = tree.create_node(&element3, instance.get_state());
244
245        // Mark only some nodes as dirty
246        scheduler.mark_dirty(node_id_1);
247        scheduler.mark_dirty(node_id_3);
248
249        // Should have 2 dirty nodes
250        assert!(scheduler.has_dirty());
251
252        render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
253
254        // All dirty nodes should be processed
255        assert!(!scheduler.has_dirty());
256    }
257}