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, reconcile_ir_node, 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_full(scheduler, tree, module, &mut DependencyGraph::new(), None)
21}
22
23/// Render only dirty nodes with data source context
24pub fn render_dirty_nodes_with_data_sources(
25    scheduler: &mut Scheduler,
26    tree: &mut InstanceTree,
27    module: Option<&ModuleInstance>,
28    data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
29) -> Vec<Patch> {
30    render_dirty_nodes_full(scheduler, tree, module, &mut DependencyGraph::new(), data_sources)
31}
32
33/// Render only dirty nodes with dependency tracking for List reconciliation
34pub fn render_dirty_nodes_with_deps(
35    scheduler: &mut Scheduler,
36    tree: &mut InstanceTree,
37    module: Option<&ModuleInstance>,
38    dependencies: &mut DependencyGraph,
39) -> Vec<Patch> {
40    render_dirty_nodes_full(scheduler, tree, module, dependencies, None)
41}
42
43/// Full render of dirty nodes with all contexts
44pub fn render_dirty_nodes_full(
45    scheduler: &mut Scheduler,
46    tree: &mut InstanceTree,
47    module: Option<&ModuleInstance>,
48    dependencies: &mut DependencyGraph,
49    data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
50) -> Vec<Patch> {
51    if !scheduler.has_dirty() {
52        return Vec::new();
53    }
54
55    let dirty_nodes = scheduler.take_dirty();
56    let state = module
57        .map(|m| m.get_state())
58        .unwrap_or(&serde_json::Value::Null);
59
60    // Store old props before updating, then update and generate patches for changed props only
61    let mut patches = Vec::new();
62    for node_id in dirty_nodes {
63        // Check if this is a List node (has array binding in raw_props AND element_template)
64        // The element_template is set for List elements that need to re-render children
65        let is_list_node = tree
66            .get(node_id)
67            .map(|n| {
68                n.raw_props
69                    .get("0")
70                    .map(|v| matches!(v, Value::Binding(_)))
71                    .unwrap_or(false)
72                    && n.element_template.is_some()
73            })
74            .unwrap_or(false);
75
76        // Check if this is a control flow node (ForEach or Conditional) with an IR template
77        let is_control_flow = tree
78            .get(node_id)
79            .map(|n| n.ir_node_template.is_some())
80            .unwrap_or(false);
81
82        if is_list_node {
83            // For List nodes, we need to re-reconcile the entire list
84            render_dirty_list(node_id, tree, state, &mut patches, dependencies, data_sources);
85        } else if is_control_flow {
86            // For control flow nodes (ForEach/Conditional from IRNode path),
87            // re-reconcile the entire subtree using the stored IR template.
88            let ir_template = tree
89                .get(node_id)
90                .and_then(|n| n.ir_node_template.clone());
91            if let Some(template) = ir_template {
92                reconcile_ir_node(
93                    tree,
94                    node_id,
95                    &template,
96                    state,
97                    &mut patches,
98                    dependencies,
99                    data_sources,
100                );
101            }
102        } else {
103            // Regular node: just update props
104            let old_props = tree.get(node_id).map(|n| n.props.clone());
105
106            // Update props with new state (and data sources if available)
107            if let Some(node) = tree.get_mut(node_id) {
108                if data_sources.is_some() {
109                    node.update_props_with_data_sources(state, data_sources);
110                } else {
111                    node.update_props(state);
112                }
113            }
114
115            // Compare and generate patches only for changed props
116            if let (Some(old), Some(node)) = (old_props, tree.get(node_id)) {
117                for (key, new_value) in &node.props {
118                    if old.get(key) != Some(new_value) {
119                        patches.push(Patch::set_prop(node_id, key.clone(), new_value.clone()));
120                    }
121                }
122                // Remove props that no longer exist
123                for key in old.keys() {
124                    if !node.props.contains_key(key) {
125                        patches.push(Patch::remove_prop(node_id, key.clone()));
126                    }
127                }
128            }
129        }
130    }
131
132    patches
133}
134
135/// Render a dirty List node by re-reconciling its children
136/// Uses keyed reconciliation for efficient updates (minimizes DOM operations)
137fn render_dirty_list(
138    node_id: NodeId,
139    tree: &mut InstanceTree,
140    state: &serde_json::Value,
141    patches: &mut Vec<Patch>,
142    dependencies: &mut DependencyGraph,
143    data_sources: Option<&indexmap::IndexMap<String, serde_json::Value>>,
144) {
145    use crate::ir::Element;
146    use crate::reconcile::item_bindings::replace_item_bindings;
147    use crate::reconcile::keyed::reconcile_keyed_children;
148
149    // Get the array binding, element template, and old children
150    let (array_binding, element_template, old_children) = {
151        let node = match tree.get(node_id) {
152            Some(n) => n,
153            None => return,
154        };
155
156        let binding = match node.raw_props.get("0") {
157            Some(Value::Binding(b)) => b.clone(),
158            _ => return,
159        };
160
161        let template = match &node.element_template {
162            Some(t) => t.clone(),
163            None => return, // No template stored, can't re-render
164        };
165
166        (binding, template, node.children.clone())
167    };
168
169    // Evaluate the array binding
170    let array = evaluate_binding(&array_binding, state).unwrap_or(serde_json::Value::Array(vec![]));
171
172    let items = match &array {
173        serde_json::Value::Array(items) => items,
174        _ => return,
175    };
176
177    // Build new child elements with keys from items
178    // Each item generates one or more children from the template
179    let mut new_children: Vec<Element> = Vec::new();
180
181    for (index, item) in items.iter().enumerate() {
182        for child_template in &element_template.children {
183            // Replace ${item.x} bindings with actual item data
184            // The key is set by replace_item_bindings:
185            // - Uses item.id or item.key for stable identity (enables efficient reordering)
186            // - Falls back to index-based key if no id field
187            let child_with_item = replace_item_bindings(child_template, item, index);
188            new_children.push(child_with_item);
189        }
190    }
191
192    // Use keyed reconciliation for efficient updates
193    // Convert im::Vector to Vec for the slice interface
194    let old_children_vec: Vec<_> = old_children.iter().copied().collect();
195    let keyed_patches = reconcile_keyed_children(
196        tree,
197        node_id,
198        &old_children_vec,
199        &new_children,
200        state,
201        dependencies,
202        data_sources,
203    );
204
205    patches.extend(keyed_patches);
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::{ir::Element, lifecycle::Module, reactive::Binding};
212    use serde_json::json;
213
214    #[test]
215    fn test_render_dirty_nodes_no_dirty() {
216        let mut scheduler = Scheduler::new();
217        let mut tree = InstanceTree::new();
218
219        let patches = render_dirty_nodes(&mut scheduler, &mut tree, None);
220        assert_eq!(patches.len(), 0);
221    }
222
223    #[test]
224    fn test_render_dirty_nodes_with_changes() {
225        let mut scheduler = Scheduler::new();
226        let mut tree = InstanceTree::new();
227
228        // Create a module with state
229        let module = Module::new("TestModule");
230        let initial_state = json!({"count": 0});
231        let instance = ModuleInstance::new(module, initial_state);
232
233        // Create a node with a binding
234        let element = Element::new("Text");
235        let node_id = tree.create_node(&element, instance.get_state());
236
237        // Mark the node as dirty
238        scheduler.mark_dirty(node_id);
239
240        // Render dirty nodes
241        let _patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
242
243        // Should have processed the dirty node (though patches may be empty if props didn't change)
244        assert!(
245            !scheduler.has_dirty(),
246            "Scheduler should have no more dirty nodes"
247        );
248    }
249
250    #[test]
251    fn test_render_dirty_nodes_state_change() {
252        use crate::ir::Value;
253
254        let mut scheduler = Scheduler::new();
255        let mut tree = InstanceTree::new();
256
257        // Create module with initial state
258        let module = Module::new("TestModule");
259        let initial_state = json!({"text": "Hello"});
260        let mut instance = ModuleInstance::new(module, initial_state);
261
262        // Create a node with a binding to state
263        let mut element = Element::new("Text");
264        element.props.insert(
265            "0".to_string(),
266            Value::Binding(Binding::state(vec!["text".to_string()])),
267        );
268        let node_id = tree.create_node(&element, instance.get_state());
269
270        // Update the node's props to reflect initial state
271        if let Some(node) = tree.get_mut(node_id) {
272            node.update_props(instance.get_state());
273        }
274
275        // Update state
276        instance.update_state(json!({"text": "World"}));
277
278        // Mark node as dirty
279        scheduler.mark_dirty(node_id);
280
281        // Render dirty nodes
282        let patches = render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
283
284        // Should generate a SetProp patch for the changed text
285        let set_prop_count = patches
286            .iter()
287            .filter(|p| matches!(p, Patch::SetProp { .. }))
288            .count();
289        assert!(
290            set_prop_count > 0,
291            "Should have SetProp patches for changed state"
292        );
293    }
294
295    #[test]
296    fn test_render_dirty_nodes_multiple_nodes() {
297        let mut scheduler = Scheduler::new();
298        let mut tree = InstanceTree::new();
299
300        let module = Module::new("TestModule");
301        let initial_state = json!({});
302        let instance = ModuleInstance::new(module, initial_state);
303
304        // Create multiple nodes
305        let element1 = Element::new("Text");
306        let element2 = Element::new("Text");
307        let element3 = Element::new("Text");
308
309        let node_id_1 = tree.create_node(&element1, instance.get_state());
310        let _node_id_2 = tree.create_node(&element2, instance.get_state());
311        let node_id_3 = tree.create_node(&element3, instance.get_state());
312
313        // Mark only some nodes as dirty
314        scheduler.mark_dirty(node_id_1);
315        scheduler.mark_dirty(node_id_3);
316
317        // Should have 2 dirty nodes
318        assert!(scheduler.has_dirty());
319
320        render_dirty_nodes(&mut scheduler, &mut tree, Some(&instance));
321
322        // All dirty nodes should be processed
323        assert!(!scheduler.has_dirty());
324    }
325}