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