Skip to main content

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, evaluate_binding},
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                    if old.get(key) != Some(new_value) {
67                        patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
68                    }
69                }
70                // Remove props that no longer exist
71                for key in old.keys() {
72                    if !node.props.contains_key(key) {
73                        patches.push(Patch::remove_prop(node_id, key.clone()));
74                    }
75                }
76            }
77        }
78    }
79
80    patches
81}
82
83/// Render a dirty List node by re-reconciling its children
84/// Uses keyed reconciliation for efficient updates (minimizes DOM operations)
85fn render_dirty_list(
86    node_id: NodeId,
87    tree: &mut InstanceTree,
88    state: &serde_json::Value,
89    patches: &mut Vec<Patch>,
90    dependencies: &mut DependencyGraph,
91) {
92    use crate::reconcile::diff::{replace_item_bindings, reconcile_keyed_children};
93    use crate::ir::Element;
94
95    // Get the array binding, element template, and old children
96    let (array_binding, element_template, old_children) = {
97        let node = match tree.get(node_id) {
98            Some(n) => n,
99            None => return,
100        };
101
102        let binding = match node.raw_props.get("0") {
103            Some(Value::Binding(b)) => b.clone(),
104            _ => return,
105        };
106
107        let template = match &node.element_template {
108            Some(t) => t.clone(),
109            None => return, // No template stored, can't re-render
110        };
111
112        (binding, template, node.children.clone())
113    };
114
115    // Evaluate the array binding
116    let array = evaluate_binding(&array_binding, state)
117        .unwrap_or(serde_json::Value::Array(vec![]));
118
119    let items = match &array {
120        serde_json::Value::Array(items) => items,
121        _ => return,
122    };
123
124    // Build new child elements with keys from items
125    // Each item generates one or more children from the template
126    let mut new_children: Vec<Element> = Vec::new();
127
128    for (index, item) in items.iter().enumerate() {
129        for child_template in &element_template.children {
130            // Replace ${item.x} bindings with actual item data
131            // The key is set by replace_item_bindings:
132            // - Uses item.id or item.key for stable identity (enables efficient reordering)
133            // - Falls back to index-based key if no id field
134            let child_with_item = replace_item_bindings(child_template, item, index);
135            new_children.push(child_with_item);
136        }
137    }
138
139    // Use keyed reconciliation for efficient updates
140    // Convert im::Vector to Vec for the slice interface
141    let old_children_vec: Vec<_> = old_children.iter().copied().collect();
142    let keyed_patches = reconcile_keyed_children(
143        tree,
144        node_id,
145        &old_children_vec,
146        &new_children,
147        state,
148        dependencies,
149    );
150
151    patches.extend(keyed_patches);
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::{ir::Element, lifecycle::Module, reactive::Binding};
158    use serde_json::json;
159
160    #[test]
161    fn test_render_dirty_nodes_no_dirty() {
162        let mut scheduler = Scheduler::new();
163        let mut tree = InstanceTree::new();
164
165        let patches = render_dirty_nodes(&mut scheduler, &mut tree, None);
166        assert_eq!(patches.len(), 0);
167    }
168
169    #[test]
170    fn test_render_dirty_nodes_with_changes() {
171        let mut scheduler = Scheduler::new();
172        let mut tree = InstanceTree::new();
173
174        // Create a module with state
175        let module = Module::new("TestModule");
176        let initial_state = json!({"count": 0});
177        let instance = ModuleInstance::new(module, initial_state);
178
179        // Create a node with a binding
180        let element = Element::new("Text");
181        let node_id = tree.create_node(&element, instance.get_state());
182
183        // Mark the node as dirty
184        scheduler.mark_dirty(node_id);
185
186        // Render dirty nodes
187        let _patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
188
189        // Should have processed the dirty node (though patches may be empty if props didn't change)
190        assert!(!scheduler.has_dirty(), "Scheduler should have no more dirty nodes");
191    }
192
193    #[test]
194    fn test_render_dirty_nodes_state_change() {
195        use crate::ir::Value;
196
197        let mut scheduler = Scheduler::new();
198        let mut tree = InstanceTree::new();
199
200        // Create module with initial state
201        let module = Module::new("TestModule");
202        let initial_state = json!({"text": "Hello"});
203        let mut instance = ModuleInstance::new(module, initial_state);
204
205        // Create a node with a binding to state
206        let mut element = Element::new("Text");
207        element
208            .props
209            .insert("0".to_string(), Value::Binding(Binding::state(vec!["text".to_string()])));
210        let node_id = tree.create_node(&element, instance.get_state());
211
212        // Update the node's props to reflect initial state
213        if let Some(node) = tree.get_mut(node_id) {
214            node.update_props(instance.get_state());
215        }
216
217        // Update state
218        instance.update_state(json!({"text": "World"}));
219
220        // Mark node as dirty
221        scheduler.mark_dirty(node_id);
222
223        // Render dirty nodes
224        let patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
225
226        // Should generate a SetProp patch for the changed text
227        let set_prop_count = patches
228            .iter()
229            .filter(|p| matches!(p, Patch::SetProp { .. }))
230            .count();
231        assert!(set_prop_count > 0, "Should have SetProp patches for changed state");
232    }
233
234    #[test]
235    fn test_render_dirty_nodes_multiple_nodes() {
236        let mut scheduler = Scheduler::new();
237        let mut tree = InstanceTree::new();
238
239        let module = Module::new("TestModule");
240        let initial_state = json!({});
241        let instance = ModuleInstance::new(module, initial_state);
242
243        // Create multiple nodes
244        let element1 = Element::new("Text");
245        let element2 = Element::new("Text");
246        let element3 = Element::new("Text");
247
248        let node_id_1 = tree.create_node(&element1, instance.get_state());
249        let _node_id_2 = tree.create_node(&element2, instance.get_state());
250        let node_id_3 = tree.create_node(&element3, instance.get_state());
251
252        // Mark only some nodes as dirty
253        scheduler.mark_dirty(node_id_1);
254        scheduler.mark_dirty(node_id_3);
255
256        // Should have 2 dirty nodes
257        assert!(scheduler.has_dirty());
258
259        render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
260
261        // All dirty nodes should be processed
262        assert!(!scheduler.has_dirty());
263    }
264}