Skip to main content

euv_core/vdom/node/
impl.rs

1use crate::*;
2
3/// Visual equality comparison for text nodes.
4///
5/// Only compares the text content; the backing signal is not considered
6/// because it does not affect visual output.
7impl PartialEq for TextNode {
8    /// Compares two text nodes by their content.
9    ///
10    /// # Arguments
11    ///
12    /// - `&Self` - The first text node.
13    /// - `&Self` - The second text node.
14    ///
15    /// # Returns
16    ///
17    /// - `bool` - `true` if the text content is equal.
18    fn eq(&self, other: &Self) -> bool {
19        self.get_content() == other.get_content()
20    }
21}
22
23/// Visual equality comparison for virtual DOM nodes.
24///
25/// Used by DynamicNode re-rendering to skip unnecessary DOM patches when
26/// the rendered output has not changed. Event attributes are always
27/// considered equal because re-binding event listeners is handled
28/// separately by the handler registry and does not affect visual output.
29/// Dynamic nodes manage their own subtree re-rendering, so two Dynamic
30/// variants are always considered equal — the inner renderer handles
31/// patching when the dynamic content actually changes.
32impl PartialEq for VirtualNode {
33    /// Compares two virtual nodes for visual equality.
34    ///
35    /// # Arguments
36    ///
37    /// - `&Self` - The first virtual node.
38    /// - `&Self` - The second virtual node.
39    ///
40    /// # Returns
41    ///
42    /// - `bool` - `true` if the virtual nodes are visually equal.
43    fn eq(&self, other: &Self) -> bool {
44        match (self, other) {
45            (VirtualNode::Text(a_text), VirtualNode::Text(b_text)) => a_text == b_text,
46            (
47                VirtualNode::Element {
48                    tag: a_tag,
49                    attributes: a_attrs,
50                    children: a_children,
51                    ..
52                },
53                VirtualNode::Element {
54                    tag: b_tag,
55                    attributes: b_attrs,
56                    children: b_children,
57                    ..
58                },
59            ) => {
60                a_tag == b_tag
61                    && a_attrs.len() == b_attrs.len()
62                    && a_attrs.iter().zip(b_attrs.iter()).all(|(a, b)| a == b)
63                    && a_children.len() == b_children.len()
64                    && a_children
65                        .iter()
66                        .zip(b_children.iter())
67                        .all(|(a, b)| a == b)
68            }
69            (VirtualNode::Fragment(a_children), VirtualNode::Fragment(b_children)) => {
70                a_children.len() == b_children.len()
71                    && a_children
72                        .iter()
73                        .zip(b_children.iter())
74                        .all(|(a, b)| a == b)
75            }
76            (VirtualNode::Dynamic(_), VirtualNode::Dynamic(_)) => false,
77            (VirtualNode::Empty, VirtualNode::Empty) => true,
78            _ => false,
79        }
80    }
81}
82
83/// Provides a default empty dynamic node with a no-op render function.
84impl Default for DynamicNode {
85    /// Returns a default `DynamicNode` with a no-op render function and empty hook context.
86    ///
87    /// # Returns
88    ///
89    /// - `Self` - A default dynamic node.
90    fn default() -> Self {
91        let node: DynamicNode = DynamicNode {
92            render_fn: Rc::new(RefCell::new(|| VirtualNode::Empty)),
93            hook_context: HookContext::default(),
94        };
95        node
96    }
97}
98
99/// Clones a `DynamicNode` by cloning its `HookContext` (Copy) and `render_fn` (Rc).
100impl Clone for DynamicNode {
101    /// Returns a clone of this dynamic node sharing the same render function.
102    ///
103    /// # Returns
104    ///
105    /// - `Self` - A cloned dynamic node.
106    fn clone(&self) -> Self {
107        DynamicNode {
108            render_fn: Rc::clone(self.get_render_fn()),
109            hook_context: self.hook_context,
110        }
111    }
112}
113
114/// Implementation of virtual node construction and property extraction.
115impl VirtualNode {
116    /// Determines whether the DOM needs to be patched when transitioning
117    /// from `old` to `new`.
118    ///
119    /// Unlike `PartialEq`, this method treats two `Dynamic` variants as
120    /// **different** so that the renderer always re-evaluates dynamic
121    /// subtrees. This is essential for route-based `match` expressions
122    /// where different pages may occupy the same DynamicNode slot.
123    ///
124    /// # Arguments
125    ///
126    /// - `&VirtualNode` - The old virtual node.
127    /// - `&VirtualNode` - The new virtual node.
128    ///
129    /// # Returns
130    ///
131    /// - `bool` - `true` if the DOM needs to be patched.
132    pub fn needs_patch(old: &VirtualNode, new: &VirtualNode) -> bool {
133        match (old, new) {
134            (VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => {
135                old_text.get_content() != new_text.get_content()
136            }
137            (
138                VirtualNode::Element {
139                    tag: old_tag,
140                    attributes: old_attrs,
141                    children: old_children,
142                    key: _old_key,
143                },
144                VirtualNode::Element {
145                    tag: new_tag,
146                    attributes: new_attrs,
147                    children: new_children,
148                    key: _new_key,
149                },
150            ) => {
151                if old_tag != new_tag {
152                    return true;
153                }
154                if old_attrs.len() != new_attrs.len() {
155                    return true;
156                }
157                for (old_attr, new_attr) in old_attrs.iter().zip(new_attrs.iter()) {
158                    if old_attr.get_name() != new_attr.get_name()
159                        || old_attr.get_value() != new_attr.get_value()
160                    {
161                        return true;
162                    }
163                }
164                if old_children.len() != new_children.len() {
165                    return true;
166                }
167                for (old_child, new_child) in old_children.iter().zip(new_children.iter()) {
168                    if Self::needs_patch(old_child, new_child) {
169                        return true;
170                    }
171                }
172                false
173            }
174            (VirtualNode::Fragment(old_children), VirtualNode::Fragment(new_children)) => {
175                if old_children.len() != new_children.len() {
176                    return true;
177                }
178                for (old_child, new_child) in old_children.iter().zip(new_children.iter()) {
179                    if Self::needs_patch(old_child, new_child) {
180                        return true;
181                    }
182                }
183                false
184            }
185            (VirtualNode::Dynamic(_), VirtualNode::Dynamic(_)) => false,
186            (VirtualNode::Empty, VirtualNode::Empty) => false,
187            _ => true,
188        }
189    }
190
191    /// Creates a new element node with the given tag name.
192    ///
193    /// # Arguments
194    ///
195    /// - `&str` - The tag name for the element.
196    ///
197    /// # Returns
198    ///
199    /// - `Self` - A new element virtual node.
200    pub fn get_element_node(tag_name: &str) -> Self {
201        VirtualNode::Element {
202            tag: Tag::Element(tag_name.to_string()),
203            attributes: Vec::new(),
204            children: Vec::new(),
205            key: None,
206        }
207    }
208
209    /// Creates a new text node with the given content.
210    ///
211    /// # Arguments
212    ///
213    /// - `&str` - The text content.
214    ///
215    /// # Returns
216    ///
217    /// - `Self` - A new text virtual node.
218    pub fn get_text_node(content: &str) -> Self {
219        VirtualNode::Text(TextNode::new(content.to_string(), None))
220    }
221
222    /// Adds an attribute to this node if it is an element.
223    ///
224    /// # Arguments
225    ///
226    /// - `&str` - The attribute name.
227    /// - `AttributeValue` - The attribute value.
228    ///
229    /// # Returns
230    ///
231    /// - `Self` - This node with the attribute added.
232    pub fn with_attribute(mut self, name: &str, value: AttributeValue) -> Self {
233        if let VirtualNode::Element {
234            ref mut attributes, ..
235        } = self
236        {
237            attributes.push(AttributeEntry::new(name.to_string(), value));
238        }
239        self
240    }
241
242    /// Adds a child node to this node if it is an element.
243    ///
244    /// # Arguments
245    ///
246    /// - `VirtualNode` - The child node to add.
247    ///
248    /// # Returns
249    ///
250    /// - `Self` - This node with the child added.
251    pub fn with_child(mut self, child: VirtualNode) -> Self {
252        if let VirtualNode::Element {
253            ref mut children, ..
254        } = self
255        {
256            children.push(child);
257        }
258        self
259    }
260
261    /// Returns true if this node is a component node.
262    ///
263    /// # Returns
264    ///
265    /// - `bool` - `true` if this is a component node.
266    pub fn is_component(&self) -> bool {
267        matches!(
268            self,
269            VirtualNode::Element {
270                tag: Tag::Component(_),
271                ..
272            }
273        )
274    }
275
276    /// Returns the tag name if this is an element or component node.
277    ///
278    /// # Returns
279    ///
280    /// - `Option<String>` - The tag name, or `None` if not an element node.
281    pub fn tag_name(&self) -> Option<String> {
282        match self {
283            VirtualNode::Element { tag, .. } => match tag {
284                Tag::Element(name) => Some(name.clone()),
285                Tag::Component(name) => Some(name.clone()),
286            },
287            _ => None,
288        }
289    }
290
291    /// Extracts a string property from this node if it is an element with the named attribute.
292    ///
293    /// # Arguments
294    ///
295    /// - `&Attribute` - The attribute to look up.
296    ///
297    /// # Returns
298    ///
299    /// - `Option<String>` - The attribute value as a string, or `None` if not found.
300    pub fn try_get_prop(&self, name: &Attribute) -> Option<String> {
301        let name_str: Cow<'static, str> = name.as_str();
302        if let VirtualNode::Element { attributes, .. } = self {
303            for attr in attributes {
304                if attr.get_name() == &name_str {
305                    match attr.get_value() {
306                        AttributeValue::Text(value) => return Some(value.clone()),
307                        AttributeValue::Signal(signal) => return Some(signal.get()),
308                        _ => {}
309                    }
310                }
311            }
312        }
313        None
314    }
315
316    /// Extracts a signal property from this node if it is an element with the named attribute.
317    ///
318    /// Returns the raw `Signal<String>` so components can reactively read the current value
319    /// and subscribe to future changes, rather than receiving a snapshot string.
320    ///
321    /// # Arguments
322    ///
323    /// - `&Attribute` - The attribute to look up.
324    ///
325    /// # Returns
326    ///
327    /// - `Option<Signal<String>>` - The signal if found, or `None`.
328    pub fn try_get_signal_prop(&self, name: &Attribute) -> Option<Signal<String>> {
329        let name_str: Cow<'static, str> = name.as_str();
330        if let VirtualNode::Element { attributes, .. } = self {
331            for attr in attributes {
332                if attr.get_name() == &name_str
333                    && let AttributeValue::Signal(signal) = attr.get_value()
334                {
335                    return Some(*signal);
336                }
337            }
338        }
339        None
340    }
341
342    /// Extracts children from this node if it is an element.
343    ///
344    /// # Returns
345    ///
346    /// - `Vec<VirtualNode>` - The children, or an empty vec if not an element.
347    pub fn get_children(&self) -> Vec<VirtualNode> {
348        if let VirtualNode::Element { children, .. } = self {
349            children.clone()
350        } else {
351            Vec::new()
352        }
353    }
354
355    /// Extracts text content from this node.
356    ///
357    /// # Returns
358    ///
359    /// - `Option<String>` - The text content, or `None` if not a text node.
360    pub fn try_get_text(&self) -> Option<String> {
361        match self {
362            VirtualNode::Text(text_node) => Some(text_node.get_content().clone()),
363            VirtualNode::Element { children, .. } => {
364                children.first().and_then(VirtualNode::try_get_text)
365            }
366            _ => None,
367        }
368    }
369
370    /// Extracts an event handler from this node if it is an element with the named event attribute.
371    ///
372    /// # Arguments
373    ///
374    /// - `&NativeEventName` - The event name to look up.
375    ///
376    /// # Returns
377    ///
378    /// - `Option<NativeEventHandler>` - The event handler if found, or `None`.
379    pub fn try_get_event(
380        &self,
381        name: &NativeEventName,
382    ) -> Option<crate::event::NativeEventHandler> {
383        let name_str: Cow<'static, str> = name.as_str();
384        if let VirtualNode::Element { attributes, .. } = self {
385            for attr in attributes {
386                if attr.get_name() == &name_str
387                    && let AttributeValue::Event(handler) = attr.get_value()
388                {
389                    return Some(handler.clone());
390                }
391            }
392        }
393        None
394    }
395
396    /// Extracts an event handler from this node by a custom attribute name.
397    ///
398    /// # Arguments
399    ///
400    /// - `&str` - The custom attribute name to look up.
401    ///
402    /// # Returns
403    ///
404    /// - `Option<NativeEventHandler>` - The event handler if found, or `None`.
405    pub fn try_get_callback(&self, name: &str) -> Option<crate::event::NativeEventHandler> {
406        if let VirtualNode::Element { attributes, .. } = self {
407            for attr in attributes {
408                if attr.get_name() == name
409                    && let AttributeValue::Event(handler) = attr.get_value()
410                {
411                    return Some(handler.clone());
412                }
413            }
414        }
415        None
416    }
417}