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}