Skip to main content

euv_core/renderer/
impl.rs

1use crate::*;
2
3/// Implementation of the virtual DOM renderer.
4impl Renderer {
5    /// Renders the given virtual DOM tree into the real DOM.
6    pub fn render(&mut self, vnode: VirtualNode) {
7        let new_unwrapped: VirtualNode = self.unwrap_component(&vnode);
8        if let Some(old_vnode) = self.try_get_current_tree() {
9            let old_unwrapped: VirtualNode = self.unwrap_component(old_vnode);
10            self.patch_root(&old_unwrapped, &new_unwrapped);
11        } else {
12            let dom_node: Node = self.create_dom_node(&new_unwrapped);
13            while let Some(child) = self.get_root().first_child() {
14                self.get_root().remove_child(&child).unwrap();
15            }
16            self.get_root().append_child(&dom_node).unwrap();
17        }
18        self.set_current_tree(Some(vnode));
19    }
20
21    /// Patches the root DOM tree by replacing the single child of `self.root`.
22    fn patch_root(&mut self, old_node: &VirtualNode, new_node: &VirtualNode) {
23        let dom_child: Option<Node> = self.get_root().first_child();
24        let is_element: bool = if let Some(ref dom_child) = dom_child {
25            dom_child.dyn_ref::<Element>().is_some()
26        } else {
27            false
28        };
29        if is_element {
30            let element: Element = dom_child.unwrap().dyn_into::<Element>().unwrap();
31            self.patch_node(old_node, new_node, &element);
32        } else if let Some(dom_child) = dom_child {
33            let new_dom: Node = self.create_dom_node(new_node);
34            self.get_root().replace_child(&new_dom, &dom_child).unwrap();
35        } else {
36            let new_dom: Node = self.create_dom_node(new_node);
37            self.get_root().append_child(&new_dom).unwrap();
38        }
39    }
40
41    /// Patches an existing DOM node to match the new virtual node.
42    fn patch_node(
43        &mut self,
44        old_node: &VirtualNode,
45        new_node: &VirtualNode,
46        dom_element: &Element,
47    ) {
48        match (old_node, new_node) {
49            (VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => {
50                if old_text.get_content() != new_text.get_content() {
51                    dom_element.set_text_content(Some(new_text.get_content()));
52                }
53            }
54            (
55                VirtualNode::Element {
56                    tag: old_tag,
57                    attributes: old_attrs,
58                    children: old_children,
59                    key: _old_key,
60                },
61                VirtualNode::Element {
62                    tag: new_tag,
63                    attributes: new_attrs,
64                    children: new_children,
65                    key: _new_key,
66                },
67            ) => {
68                if !Self::tags_equal(old_tag, new_tag) {
69                    let new_dom: Node = self.create_dom_node(new_node);
70                    if let Some(parent) = dom_element.parent_node() {
71                        parent.replace_child(&new_dom, dom_element).unwrap();
72                    }
73                    return;
74                }
75                self.patch_attributes(dom_element, old_attrs, new_attrs);
76                self.patch_children(dom_element, old_children, new_children);
77            }
78            (VirtualNode::Fragment(old_children), VirtualNode::Fragment(new_children)) => {
79                self.patch_children(dom_element, old_children, new_children);
80            }
81            _ => {
82                let new_dom: Node = self.create_dom_node(new_node);
83                if let Some(parent) = dom_element.parent_node() {
84                    parent.replace_child(&new_dom, dom_element).unwrap();
85                }
86            }
87        }
88    }
89
90    /// Patches attributes of an element, adding, removing, or updating as needed.
91    fn patch_attributes(
92        &mut self,
93        element: &Element,
94        old_attrs: &[AttributeEntry],
95        new_attrs: &[AttributeEntry],
96    ) {
97        let mut old_map: HashMap<&str, &AttributeValue> = HashMap::new();
98        for attr in old_attrs {
99            old_map.insert(attr.get_name(), attr.get_value());
100        }
101        let mut new_map: HashMap<&str, &AttributeValue> = HashMap::new();
102        for attr in new_attrs {
103            new_map.insert(attr.get_name(), attr.get_value());
104        }
105        for name in old_map.keys() {
106            if !new_map.contains_key(*name) {
107                Self::remove_dom_attribute_or_property(element, name);
108            }
109        }
110        for attr in new_attrs {
111            let should_set: bool = match old_map.get(attr.get_name().as_str()) {
112                Some(old_value) => !Self::attribute_values_equal(old_value, attr.get_value()),
113                None => true,
114            };
115            if should_set {
116                match attr.get_value() {
117                    AttributeValue::Text(value) => {
118                        if value.is_empty() {
119                            Self::remove_dom_attribute_or_property(element, attr.get_name());
120                        } else {
121                            Self::set_dom_attribute_or_property(element, attr.get_name(), value);
122                        }
123                    }
124                    AttributeValue::Signal(signal) => {
125                        let value: String = signal.get();
126                        if value.is_empty() && !Self::is_boolean_property(attr.get_name()) {
127                            Self::remove_dom_attribute_or_property(element, attr.get_name());
128                        } else {
129                            Self::set_dom_attribute_or_property(element, attr.get_name(), &value);
130                        }
131                    }
132                    AttributeValue::Event(handler) => {
133                        self.attach_event_listener(element, handler);
134                    }
135                    AttributeValue::Dynamic(_) => {}
136                    AttributeValue::Css(css_class) => {
137                        css_class.inject_style();
138                        Self::set_dom_attribute_or_property(
139                            element,
140                            attr.get_name(),
141                            css_class.get_name(),
142                        );
143                    }
144                }
145            }
146        }
147    }
148
149    /// Returns true if the given attribute name is a boolean attribute that
150    /// requires DOM property-based manipulation instead of HTML attribute strings.
151    fn is_boolean_property(name: &str) -> bool {
152        matches!(name, "checked" | "disabled" | "selected" | "readonly")
153    }
154
155    /// Removes or clears a DOM attribute/property, depending on the attribute name.
156    ///
157    /// For `value`, sets the DOM property to an empty string rather than calling
158    /// `remove_attribute`, because `remove_attribute("value")` only removes the
159    /// HTML attribute and does not clear the displayed value of input elements.
160    /// For boolean properties (`checked`, `disabled`, `selected`, `readonly`),
161    /// sets the DOM property to `false` rather than calling `remove_attribute`,
162    /// because `remove_attribute` on a previously-set attribute may not correctly
163    /// reset the property in all browsers.
164    fn remove_dom_attribute_or_property(element: &Element, name: &str) {
165        if name == "value" {
166            if let Some(input) = element.dyn_ref::<HtmlInputElement>() {
167                input.set_value("");
168                return;
169            }
170            if let Some(textarea) = element.dyn_ref::<HtmlTextAreaElement>() {
171                textarea.set_value("");
172                return;
173            }
174            if let Some(select) = element.dyn_ref::<HtmlSelectElement>() {
175                select.set_value("");
176                return;
177            }
178        }
179        if name == "checked"
180            && let Some(input) = element.dyn_ref::<HtmlInputElement>()
181        {
182            input.set_checked(false);
183            return;
184        }
185        if name == "disabled" {
186            if let Some(input) = element.dyn_ref::<HtmlInputElement>() {
187                input.set_disabled(false);
188                return;
189            }
190            if let Some(button) = element.dyn_ref::<HtmlButtonElement>() {
191                button.set_disabled(false);
192                return;
193            }
194            if let Some(select) = element.dyn_ref::<HtmlSelectElement>() {
195                select.set_disabled(false);
196                return;
197            }
198            if let Some(textarea) = element.dyn_ref::<HtmlTextAreaElement>() {
199                textarea.set_disabled(false);
200                return;
201            }
202        }
203        if name == "selected"
204            && let Some(option) = element.dyn_ref::<HtmlOptionElement>()
205        {
206            option.set_selected(false);
207            return;
208        }
209        if name == "readonly" {
210            if let Some(input) = element.dyn_ref::<HtmlInputElement>() {
211                input.set_read_only(false);
212                return;
213            }
214            if let Some(textarea) = element.dyn_ref::<HtmlTextAreaElement>() {
215                textarea.set_read_only(false);
216                return;
217            }
218        }
219        let _ = element.remove_attribute(name);
220    }
221
222    /// Sets a DOM attribute or property, depending on the attribute name.
223    ///
224    /// For `value`, uses the DOM property to ensure input elements update correctly.
225    /// For boolean attributes (`checked`, `disabled`, `selected`, `readonly`),
226    /// uses the DOM property so that the browser honors the value correctly
227    /// (HTML attributes are present-or-absent, not true/false strings).
228    /// For all other attributes, uses `set_attribute`.
229    fn set_dom_attribute_or_property(element: &Element, name: &str, value: &str) {
230        if name == "value" {
231            if let Some(input) = element.dyn_ref::<HtmlInputElement>() {
232                input.set_value(value);
233                return;
234            }
235            if let Some(textarea) = element.dyn_ref::<HtmlTextAreaElement>() {
236                textarea.set_value(value);
237                return;
238            }
239            if let Some(select) = element.dyn_ref::<HtmlSelectElement>() {
240                select.set_value(value);
241                return;
242            }
243        }
244        if name == "checked"
245            && let Some(input) = element.dyn_ref::<HtmlInputElement>()
246        {
247            input.set_checked(value == "true");
248            return;
249        }
250        if name == "disabled" {
251            if let Some(input) = element.dyn_ref::<HtmlInputElement>() {
252                input.set_disabled(value == "true");
253                return;
254            }
255            if let Some(button) = element.dyn_ref::<HtmlButtonElement>() {
256                button.set_disabled(value == "true");
257                return;
258            }
259            if let Some(select) = element.dyn_ref::<HtmlSelectElement>() {
260                select.set_disabled(value == "true");
261                return;
262            }
263            if let Some(textarea) = element.dyn_ref::<HtmlTextAreaElement>() {
264                textarea.set_disabled(value == "true");
265                return;
266            }
267        }
268        if name == "selected"
269            && let Some(option) = element.dyn_ref::<HtmlOptionElement>()
270        {
271            option.set_selected(value == "true");
272            return;
273        }
274        if name == "readonly" {
275            if let Some(input) = element.dyn_ref::<HtmlInputElement>() {
276                input.set_read_only(value == "true");
277                return;
278            }
279            if let Some(textarea) = element.dyn_ref::<HtmlTextAreaElement>() {
280                textarea.set_read_only(value == "true");
281                return;
282            }
283        }
284        let _ = element.set_attribute(name, value);
285    }
286
287    /// Compares two tags for equality.
288    fn tags_equal(a: &Tag, b: &Tag) -> bool {
289        match (a, b) {
290            (Tag::Element(a_name), Tag::Element(b_name)) => a_name == b_name,
291            (Tag::Component(a_name), Tag::Component(b_name)) => a_name == b_name,
292            _ => false,
293        }
294    }
295
296    /// Compares two attribute values for equality.
297    ///
298    /// Event attributes are always considered unequal to ensure that
299    /// event listeners are re-bound on every patch. This is critical
300    /// because the underlying closure may capture different signal
301    /// references after a route change, even though the event name
302    /// remains the same.
303    fn attribute_values_equal(a: &AttributeValue, b: &AttributeValue) -> bool {
304        match (a, b) {
305            (AttributeValue::Text(a_val), AttributeValue::Text(b_val)) => a_val == b_val,
306            (AttributeValue::Signal(_a_sig), AttributeValue::Signal(_b_sig)) => false,
307            (AttributeValue::Event(_a_ev), AttributeValue::Event(_b_ev)) => false,
308            (AttributeValue::Dynamic(a_dyn), AttributeValue::Dynamic(b_dyn)) => a_dyn == b_dyn,
309            (AttributeValue::Css(a_css), AttributeValue::Css(b_css)) => {
310                a_css.get_name() == b_css.get_name()
311            }
312            _ => false,
313        }
314    }
315
316    /// Gets a child node at the given index by traversing child nodes.
317    fn get_child_node(parent: &Element, index: u32) -> Option<Node> {
318        let mut current: Option<Node> = parent.first_child();
319        let mut current_index: u32 = 0;
320        while let Some(node) = current {
321            if current_index == index {
322                return Some(node);
323            }
324            current = node.next_sibling();
325            current_index += 1;
326        }
327        None
328    }
329
330    /// Patches children of an element using a positional diff algorithm.
331    ///
332    /// For each position, patches the old child into the new child in-place.
333    /// Text nodes are updated by modifying their text content rather than
334    /// being replaced, which preserves any reactive signal subscriptions
335    /// already wired to the existing DOM text node.
336    /// Appends any extra new children, and removes any trailing old children.
337    fn patch_children(
338        &mut self,
339        parent: &Element,
340        old_children: &[VirtualNode],
341        new_children: &[VirtualNode],
342    ) {
343        let old_len: usize = old_children.len();
344        let new_len: usize = new_children.len();
345        let common_len: usize = old_len.min(new_len);
346        for index in 0..common_len {
347            let old_child: &VirtualNode = &old_children[index];
348            let new_child: &VirtualNode = &new_children[index];
349            if let Some(dom_child) = Self::get_child_node(parent, index as u32) {
350                if let Some(element) = dom_child.dyn_ref::<Element>() {
351                    self.patch_node(old_child, new_child, element);
352                } else if let (VirtualNode::Text(old_text), VirtualNode::Text(new_text)) =
353                    (old_child, new_child)
354                {
355                    if old_text.get_content() != new_text.get_content() {
356                        dom_child.set_text_content(Some(new_text.get_content()));
357                    }
358                } else {
359                    let new_dom: Node = self.create_dom_node(new_child);
360                    if let Some(parent_node) = dom_child.parent_node() {
361                        let _ = parent_node.replace_child(&new_dom, &dom_child);
362                    }
363                }
364            }
365        }
366        if new_len > old_len {
367            for new_child in new_children.iter().skip(common_len) {
368                let new_dom: Node = self.create_dom_node(new_child);
369                parent.append_child(&new_dom).unwrap();
370            }
371        } else if old_len > new_len {
372            for _ in common_len..old_len {
373                if let Some(last_child) = parent.last_child() {
374                    parent.remove_child(&last_child).unwrap();
375                }
376            }
377        }
378    }
379
380    /// Creates a real DOM node from a virtual node.
381    fn create_dom_node(&mut self, node: &VirtualNode) -> Node {
382        match node {
383            VirtualNode::Element {
384                tag,
385                attributes,
386                children,
387                ..
388            } => {
389                let document: Document = window().unwrap().document().unwrap();
390                let element: Element = match tag {
391                    Tag::Element(name) => document.create_element(name).unwrap(),
392                    Tag::Component(_) => {
393                        let unwrapped: VirtualNode = self.unwrap_component(node);
394                        return self.create_dom_node(&unwrapped);
395                    }
396                };
397                for attr in attributes {
398                    match attr.get_value() {
399                        AttributeValue::Text(value) => {
400                            if !value.is_empty() || Self::is_boolean_property(attr.get_name()) {
401                                Self::set_dom_attribute_or_property(
402                                    &element,
403                                    attr.get_name(),
404                                    value,
405                                );
406                            }
407                        }
408                        AttributeValue::Signal(signal) => {
409                            let initial_value: String = signal.get();
410                            if !initial_value.is_empty()
411                                || Self::is_boolean_property(attr.get_name())
412                            {
413                                Self::set_dom_attribute_or_property(
414                                    &element,
415                                    attr.get_name(),
416                                    &initial_value,
417                                );
418                            }
419                            let attr_name: String = attr.get_name().clone();
420                            let element_clone: Element = element.clone();
421                            let signal_for_sub: Signal<String> = *signal;
422                            let signal_inner: Signal<String> = signal_for_sub;
423                            signal_for_sub.subscribe(move || {
424                                let new_value: String = signal_inner.get();
425                                if new_value.is_empty() && !Self::is_boolean_property(&attr_name) {
426                                    Self::remove_dom_attribute_or_property(
427                                        &element_clone,
428                                        &attr_name,
429                                    );
430                                } else {
431                                    Self::set_dom_attribute_or_property(
432                                        &element_clone,
433                                        &attr_name,
434                                        &new_value,
435                                    );
436                                }
437                            });
438                        }
439                        AttributeValue::Event(handler) => {
440                            self.attach_event_listener(&element, handler);
441                        }
442                        AttributeValue::Dynamic(_) => {}
443                        AttributeValue::Css(css_class) => {
444                            css_class.inject_style();
445                            Self::set_dom_attribute_or_property(
446                                &element,
447                                attr.get_name(),
448                                css_class.get_name(),
449                            );
450                        }
451                    }
452                }
453                for child in children {
454                    let child_node: Node = self.create_dom_node(child);
455                    element.append_child(&child_node).unwrap();
456                }
457                element.into()
458            }
459            VirtualNode::Text(text_node) => {
460                let document: Document = window().unwrap().document().unwrap();
461                let text: Text = document.create_text_node(text_node.get_content());
462                if let Some(signal) = text_node.try_get_signal() {
463                    let text_clone: Text = text.clone();
464                    let signal_clone: Signal<String> = *signal;
465                    signal_clone.subscribe({
466                        let signal_inner: Signal<String> = signal_clone;
467                        move || {
468                            let new_value: String = signal_inner.get();
469                            text_clone.set_text_content(Some(&new_value));
470                        }
471                    });
472                }
473                text.into()
474            }
475            VirtualNode::Fragment(children) => {
476                let document: Document = window().unwrap().document().unwrap();
477                let fragment: Element = document.create_element("div").unwrap();
478                for child in children {
479                    let child_node: Node = self.create_dom_node(child);
480                    fragment.append_child(&child_node).unwrap();
481                }
482                fragment.into()
483            }
484            VirtualNode::Dynamic(dynamic_node) => {
485                let document: Document = window().unwrap().document().unwrap();
486                let placeholder: Element = document.create_element("div").unwrap();
487                let style: &str = "display: contents;";
488                let _ = placeholder.set_attribute("style", style);
489                let mut hook_context: HookContext = dynamic_node.hook_context;
490                hook_context.reset_hook_index();
491                let initial_vnode: VirtualNode = with_hook_context(hook_context, || {
492                    let mut borrowed = dynamic_node.render_fn.borrow_mut();
493                    borrowed()
494                });
495                let initial_unwrapped: VirtualNode = self.unwrap_component(&initial_vnode);
496                let initial_dom: Node = self.create_dom_node(&initial_unwrapped);
497                placeholder.append_child(&initial_dom).unwrap();
498                let render_fn_clone: Rc<RefCell<dyn FnMut() -> VirtualNode>> =
499                    Rc::clone(&dynamic_node.render_fn);
500                let placeholder_clone: Element = placeholder.clone();
501                let mut renderer_for_sub: Renderer = Renderer::new(placeholder_clone.clone());
502                renderer_for_sub.set_current_tree(Some(initial_unwrapped));
503                let renderer_ref: Rc<RefCell<Renderer>> = Rc::new(RefCell::new(renderer_for_sub));
504                let renderer_ref_for_sub: Rc<RefCell<Renderer>> = Rc::clone(&renderer_ref);
505                let render_fn_for_sub: Rc<RefCell<dyn FnMut() -> VirtualNode>> =
506                    Rc::clone(&render_fn_clone);
507                let window: Window = window().unwrap();
508                let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
509                    if placeholder_clone.parent_node().is_none() {
510                        return;
511                    }
512                    hook_context.reset_hook_index();
513                    let new_vnode: VirtualNode = with_hook_context(hook_context, || {
514                        let mut borrowed = render_fn_for_sub.borrow_mut();
515                        borrowed()
516                    });
517                    let mut renderer = renderer_ref_for_sub.borrow_mut();
518                    renderer.render(new_vnode);
519                }));
520                window
521                    .add_event_listener_with_callback(
522                        &NativeEventName::EuvSignalUpdate.to_string(),
523                        closure.as_ref().unchecked_ref(),
524                    )
525                    .unwrap();
526                closure.forget();
527                placeholder.into()
528            }
529            VirtualNode::Empty => {
530                let document: Document = window().unwrap().document().unwrap();
531                document.create_text_node("").into()
532            }
533        }
534    }
535
536    /// Recursively unwraps component nodes into their rendered output.
537    fn unwrap_component(&self, node: &VirtualNode) -> VirtualNode {
538        match node {
539            VirtualNode::Element {
540                tag: Tag::Component(_),
541                children,
542                ..
543            } => {
544                if children.len() == 1 {
545                    self.unwrap_component(&children[0])
546                } else {
547                    VirtualNode::Fragment(children.clone())
548                }
549            }
550            VirtualNode::Element {
551                tag,
552                attributes,
553                children,
554                key,
555            } => {
556                let unwrapped_children: Vec<VirtualNode> = children
557                    .iter()
558                    .map(|child| self.unwrap_component(child))
559                    .collect();
560                VirtualNode::Element {
561                    tag: tag.clone(),
562                    attributes: attributes.clone(),
563                    children: unwrapped_children,
564                    key: key.clone(),
565                }
566            }
567            VirtualNode::Fragment(children) => {
568                let unwrapped_children: Vec<VirtualNode> = children
569                    .iter()
570                    .map(|child| self.unwrap_component(child))
571                    .collect();
572                VirtualNode::Fragment(unwrapped_children)
573            }
574            other => other.clone(),
575        }
576    }
577
578    /// Attaches an event listener to a DOM element.
579    ///
580    /// Uses a global auto-incrementing ID stored as `data-euv-id` on the element
581    /// to uniquely identify it in the handler registry. This avoids the bug where
582    /// `element.as_ref() as *const JsValue as usize` returns the address of the
583    /// Rust-side temporary `JsValue` wrapper rather than a stable JS object identity,
584    /// causing different DOM elements to collide on the same key.
585    ///
586    /// On first attach, allocates a new ID, creates a wrapper
587    /// `Rc<RefCell<Option<NativeEventHandler>>>`, and registers a DOM
588    /// `addEventListener` closure that reads from it. On subsequent patches
589    /// for the same element+event, only updates the wrapper content.
590    fn attach_event_listener(&self, element: &Element, handler: &NativeEventHandler) {
591        let euv_id: usize = match element.get_attribute("data-euv-id") {
592            Some(id_str) => id_str.parse::<usize>().unwrap_or_else(|_| {
593                let new_id: usize = NEXT_EUV_ID.fetch_add(1, Ordering::Relaxed);
594                let _ = element.set_attribute("data-euv-id", &new_id.to_string());
595                new_id
596            }),
597            None => {
598                let new_id: usize = NEXT_EUV_ID.fetch_add(1, Ordering::Relaxed);
599                let _ = element.set_attribute("data-euv-id", &new_id.to_string());
600                new_id
601            }
602        };
603        let event_name: String = handler.get_event_name().clone();
604        let key: (usize, String) = (euv_id, event_name.clone());
605        // SAFETY: WASM is single-threaded; no concurrent access to HANDLER_REGISTRY.
606        let registry: &mut HashMap<(usize, String), Rc<RefCell<Option<NativeEventHandler>>>> =
607            get_handler_registry();
608        if let Some(existing_wrapper) = registry.get(&key) {
609            let mut wrapper: RefMut<Option<NativeEventHandler>> = existing_wrapper.borrow_mut();
610            *wrapper = Some(handler.clone());
611        } else {
612            let handler_wrapper: Rc<RefCell<Option<NativeEventHandler>>> =
613                Rc::new(RefCell::new(Some(handler.clone())));
614            let wrapper_for_closure: Rc<RefCell<Option<NativeEventHandler>>> =
615                Rc::clone(&handler_wrapper);
616            let event_name_for_closure: String = event_name.clone();
617            let closure: Closure<dyn FnMut(Event)> =
618                Closure::wrap(Box::new(move |event: Event| {
619                    if let Some(active_handler) = wrapper_for_closure.borrow_mut().as_ref() {
620                        let euv_event: NativeEvent =
621                            convert_web_event(&event, &event_name_for_closure);
622                        active_handler.handle(euv_event);
623                    }
624                }));
625            element
626                .add_event_listener_with_callback(&event_name, closure.as_ref().unchecked_ref())
627                .unwrap();
628            closure.forget();
629            registry.insert(key, handler_wrapper);
630        }
631    }
632}