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.get_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 /// - `&str` - The attribute name 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: &str) -> Option<String> {
301 if let VirtualNode::Element { attributes, .. } = self {
302 for attr in attributes {
303 if attr.get_name() == name {
304 match attr.get_value() {
305 AttributeValue::Text(value) => return Some(value.clone()),
306 AttributeValue::Signal(signal) => return Some(signal.get()),
307 AttributeValue::Dynamic(value) => return Some(value.clone()),
308 _ => {}
309 }
310 }
311 }
312 }
313 None
314 }
315
316 /// Extracts a typed property from this node by parsing the attribute value string.
317 ///
318 /// Supports `Text`, `Signal`, and `Dynamic` attribute values. The string
319 /// representation is parsed into the target type `T` via `FromStr`.
320 ///
321 /// # Arguments
322 ///
323 /// - `&str` - The attribute name to look up.
324 ///
325 /// # Returns
326 ///
327 /// - `Option<T>` - The parsed value, or `None` if not found or parsing fails.
328 pub fn try_get_typed_prop<T>(&self, name: &str) -> Option<T>
329 where
330 T: std::str::FromStr,
331 {
332 if let VirtualNode::Element { attributes, .. } = self {
333 for attr in attributes {
334 if attr.get_name() == name {
335 let raw: String = match attr.get_value() {
336 AttributeValue::Text(value) => value.clone(),
337 AttributeValue::Signal(signal) => signal.get(),
338 AttributeValue::Dynamic(value) => value.clone(),
339 _ => continue,
340 };
341 return raw.parse::<T>().ok();
342 }
343 }
344 }
345 None
346 }
347
348 /// Extracts a signal property from this node if it is an element with the named attribute.
349 ///
350 /// Returns the raw `Signal<String>` so components can reactively read the current value
351 /// and subscribe to future changes, rather than receiving a snapshot string.
352 ///
353 /// # Arguments
354 ///
355 /// - `&str` - The attribute name to look up.
356 ///
357 /// # Returns
358 ///
359 /// - `Option<Signal<String>>` - The signal if found, or `None`.
360 pub fn try_get_signal_prop(&self, name: &str) -> Option<Signal<String>> {
361 if let VirtualNode::Element { attributes, .. } = self {
362 for attr in attributes {
363 if attr.get_name() == name
364 && let AttributeValue::Signal(signal) = attr.get_value()
365 {
366 return Some(*signal);
367 }
368 }
369 }
370 None
371 }
372
373 /// Extracts children from this node if it is an element.
374 ///
375 /// # Returns
376 ///
377 /// - `Vec<VirtualNode>` - The children, or an empty vec if not an element.
378 pub fn get_children(&self) -> Vec<VirtualNode> {
379 if let VirtualNode::Element { children, .. } = self {
380 children.clone()
381 } else {
382 Vec::new()
383 }
384 }
385
386 /// Extracts text content from this node.
387 ///
388 /// # Returns
389 ///
390 /// - `Option<String>` - The text content, or `None` if not a text node.
391 pub fn try_get_text(&self) -> Option<String> {
392 match self {
393 VirtualNode::Text(text_node) => Some(text_node.get_content().clone()),
394 VirtualNode::Element { children, .. } => {
395 children.first().and_then(VirtualNode::try_get_text)
396 }
397 _ => None,
398 }
399 }
400
401 /// Extracts an event handler from this node if it is an element with the named event attribute.
402 ///
403 /// # Arguments
404 ///
405 /// - `&str` - The event name to look up.
406 ///
407 /// # Returns
408 ///
409 /// - `Option<NativeEventHandler>` - The event handler if found, or `None`.
410 pub fn try_get_event(&self, name: &str) -> Option<NativeEventHandler> {
411 if let VirtualNode::Element { attributes, .. } = self {
412 for attr in attributes {
413 if attr.get_name() == name
414 && let AttributeValue::Event(handler) = attr.get_value()
415 {
416 return Some(handler.clone());
417 }
418 }
419 }
420 None
421 }
422
423 /// Extracts an event handler from this node by a custom attribute name.
424 ///
425 /// # Arguments
426 ///
427 /// - `&str` - The custom attribute name to look up.
428 ///
429 /// # Returns
430 ///
431 /// - `Option<NativeEventHandler>` - The event handler if found, or `None`.
432 pub fn try_get_callback(&self, name: &str) -> Option<NativeEventHandler> {
433 if let VirtualNode::Element { attributes, .. } = self {
434 for attr in attributes {
435 if attr.get_name() == name
436 && let AttributeValue::Event(handler) = attr.get_value()
437 {
438 return Some(handler.clone());
439 }
440 }
441 }
442 None
443 }
444}