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