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