euv_core/vdom/node/impl.rs
1use super::*;
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.
7///
8/// OPT 29: `content` is `Cow<'static, str>`; compare the dereffed
9/// string views (cheap for both `Borrowed` and `Owned`).
10impl PartialEq for TextNode {
11 /// Returns `true` when `self` and `other` are equivalent by the [`PartialEq`] contract.
12 ///
13 /// # Arguments
14 ///
15 /// - `&Self` - The other value to compare against `self`.
16 ///
17 /// # Returns
18 ///
19 /// - `bool` - `true` when `self` and `other` are equivalent by the trait contract.
20 fn eq(&self, other: &Self) -> bool {
21 self.get_content().as_ref() == other.get_content().as_ref()
22 }
23}
24
25/// Clones a `VirtualNode<T>` by deep-copying all fields.
26impl<T: Clone> Clone for VirtualNode<T> {
27 /// Clones the [`VirtualNode`] by reusing shared, cheap-to-clone state where possible.
28 fn clone(&self) -> Self {
29 match self {
30 Self::Element {
31 tag,
32 attributes,
33 children,
34 key,
35 props,
36 } => Self::Element {
37 tag: tag.clone(),
38 attributes: attributes.clone(),
39 children: children.clone(),
40 key: key.clone(),
41 props: props.clone(),
42 },
43 Self::Text(text_node) => Self::Text(text_node.clone()),
44 Self::Fragment(children) => Self::Fragment(children.clone()),
45 Self::Dynamic(dynamic_node) => Self::Dynamic(dynamic_node.clone()),
46 Self::Empty => Self::Empty,
47 }
48 }
49}
50
51/// Debug formatting for `VirtualNode<T>`.
52///
53/// Skips `Dynamic` inner details and `props` for brevity.
54impl<T: Debug> Debug for VirtualNode<T> {
55 /// Formats the [`VirtualNode`] via the supplied formatter.
56 ///
57 /// # Arguments
58 ///
59 /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
60 ///
61 /// # Returns
62 ///
63 /// - `fmt::Result` - Result of the formatting operation.
64 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Element {
67 tag,
68 attributes,
69 children,
70 key,
71 props,
72 } => formatter
73 .debug_struct("Element")
74 .field("tag", tag)
75 .field("attributes", attributes)
76 .field("children", children)
77 .field("key", key)
78 .field("props", props)
79 .finish(),
80 Self::Text(text_node) => formatter.debug_tuple("Text").field(text_node).finish(),
81 Self::Fragment(children) => formatter.debug_tuple("Fragment").field(children).finish(),
82 Self::Dynamic(_) => formatter.debug_tuple("Dynamic").finish(),
83 Self::Empty => formatter.debug_tuple("Empty").finish(),
84 }
85 }
86}
87
88/// Default implementation returns `VirtualNode::Empty`.
89impl<T> Default for VirtualNode<T> {
90 /// Constructs a default [`VirtualNode`] value.
91 fn default() -> Self {
92 Self::Empty
93 }
94}
95
96/// Visual equality comparison for virtual DOM nodes.
97///
98/// Used by DynamicNode re-rendering to skip unnecessary DOM patches when
99/// the rendered output has not changed. Event attributes are always
100/// considered equal because re-binding event listeners is handled
101/// separately by the handler registry and does not affect visual output.
102/// Dynamic nodes manage their own subtree re-rendering, so two Dynamic
103/// variants are always considered equal — the inner renderer handles
104/// patching when the dynamic content actually changes.
105impl<T: PartialEq> PartialEq for VirtualNode<T> {
106 /// Returns `true` when `self` and `other` are equivalent by the [`PartialEq`] contract.
107 ///
108 /// # Arguments
109 ///
110 /// - `&Self` - The other value to compare against `self`.
111 ///
112 /// # Returns
113 ///
114 /// - `bool` - `true` when `self` and `other` are equivalent by the trait contract.
115 fn eq(&self, other: &Self) -> bool {
116 match (self, other) {
117 (VirtualNode::Text(old_text), VirtualNode::Text(new_text)) => old_text == new_text,
118 (
119 VirtualNode::Element {
120 tag: old_tag,
121 attributes: old_attrs,
122 children: old_children,
123 props: old_props,
124 ..
125 },
126 VirtualNode::Element {
127 tag: new_tag,
128 attributes: new_attrs,
129 children: new_children,
130 props: new_props,
131 ..
132 },
133 ) => {
134 old_tag == new_tag
135 && old_attrs.len() == new_attrs.len()
136 && old_attrs.iter().zip(new_attrs.iter()).all(
137 |(old_attr, new_attr): (&AttributeEntry, &AttributeEntry)| {
138 old_attr == new_attr
139 },
140 )
141 && old_children.len() == new_children.len()
142 && old_children.iter().zip(new_children.iter()).all(
143 |(old_child, new_child): (&VirtualNode, &VirtualNode)| {
144 old_child == new_child
145 },
146 )
147 && old_props == new_props
148 }
149 (VirtualNode::Fragment(old_children), VirtualNode::Fragment(new_children)) => {
150 old_children.len() == new_children.len()
151 && old_children.iter().zip(new_children.iter()).all(
152 |(old_child, new_child): (&VirtualNode, &VirtualNode)| {
153 old_child == new_child
154 },
155 )
156 }
157 (VirtualNode::Dynamic(_), VirtualNode::Dynamic(_)) => false,
158 (VirtualNode::Empty, VirtualNode::Empty) => true,
159 _ => false,
160 }
161 }
162}
163
164/// Provides a default empty dynamic node with a no-op render function.
165impl Default for DynamicNode {
166 /// Constructs a default [`DynamicNode`] value.
167 fn default() -> Self {
168 let render_fn_inner: Rc<UnsafeCell<RenderFnInner>> = Rc::new(UnsafeCell::new(
169 RenderFnInner::new(Box::new(|_: &mut HookContext| VirtualNode::Empty)),
170 ));
171 Self::new(render_fn_inner, HookContext::default())
172 }
173}
174
175/// Implementation of dynamic node accessor methods.
176impl DynamicNode {
177 /// Invokes the render closure and returns the produced virtual node.
178 ///
179 /// # Safety
180 ///
181 /// Must only be called from the main thread. Guaranteed in WASM
182 /// single-threaded context. No concurrent access is possible.
183 ///
184 /// # Arguments
185 ///
186 /// - `&mut HookContext` - The hook context to pass to the render closure.
187 ///
188 /// # Returns
189 ///
190 /// - `VirtualNode` - The virtual node produced by the render closure.
191 pub fn render(&self, hook_context: &mut HookContext) -> VirtualNode {
192 let inner: &mut RenderFnInner = unsafe { &mut *self.get_render_fn().get() };
193 (inner.get_mut_render_fn())(hook_context)
194 }
195}
196
197/// Implementation of virtual node construction and property extraction.
198impl<T> VirtualNode<T> {
199 /// Returns the tag name if this is an element or component node.
200 ///
201 /// # Returns
202 ///
203 /// - `Option<String>` - The tag name, or `None` if not an element.
204 pub fn try_get_tag_name(&self) -> Option<String> {
205 match self {
206 Self::Element { tag, .. } => match tag {
207 // OPT 2: `Cow::to_string()` allocates only for the
208 // `Owned` branch. The common `Borrowed("div")` path
209 // performs one string slice clone (no heap).
210 Tag::Element(name) => Some(name.to_string()),
211 Tag::Component(name) => Some(name.to_string()),
212 // Portals do not contribute a tag name to the
213 // declared position in the DOM tree — their content
214 // is rendered into a separate target, and the
215 // marker is an internal implementation detail.
216 // Returning `None` here keeps callers that use
217 // `try_get_tag_name` for "what tag is this?" away
218 // from the portal sentinel.
219 Tag::Portal(_) => None,
220 },
221 _ => None,
222 }
223 }
224
225 /// Returns the children of this node as a borrowed slice.
226 ///
227 /// Returns an empty slice for `Empty`, the children of `Element`
228 /// and `Fragment` variants, and an empty slice for `Text` /
229 /// `Dynamic`. Zero-copy; callers can iterate without cloning.
230 ///
231 /// # Returns
232 ///
233 /// - `&[VirtualNode]` - The children, or an empty slice.
234 pub fn get_children(&self) -> &[VirtualNode] {
235 match self {
236 Self::Element { children, .. } => children.as_slice(),
237 Self::Fragment(children) => children.as_slice(),
238 _ => &[],
239 }
240 }
241
242 /// Returns the first child of this node as a borrowed reference,
243 /// if any.
244 ///
245 /// Returns `None` when there are no children; otherwise returns
246 /// a reference to the first child. Zero-copy; replaces the
247 /// previous `get_child_node` helper that cloned the entire
248 /// children subtree per render.
249 ///
250 /// # Returns
251 ///
252 /// - `Option<&VirtualNode>` - The first child, or `None`.
253 pub fn get_first_child(&self) -> Option<&VirtualNode> {
254 self.get_children().first()
255 }
256
257 /// Returns `true` if this node has non-empty children.
258 ///
259 /// # Returns
260 ///
261 /// - `bool` - Whether this node has children.
262 pub fn has_children(&self) -> bool {
263 !self.get_children().is_empty()
264 }
265
266 /// Clones the props of this node.
267 ///
268 /// # Returns
269 ///
270 /// - `Option<T>` - The cloned props, or `None` if this node has no props.
271 pub fn try_get_props(&self) -> Option<T>
272 where
273 T: Clone,
274 {
275 match self {
276 Self::Element { props, .. } => props.as_deref().cloned(),
277 _ => None,
278 }
279 }
280
281 /// Returns the children of this node as a borrowed slice.
282 ///
283 /// Equivalent to [`Self::get_children`] but exposes the raw
284 /// `Option<&[VirtualNode]>` shape for callers that want to
285 /// distinguish "no children" from "empty children" (Element
286 /// without children vs. Text/Dynamic/Empty).
287 ///
288 /// # Returns
289 ///
290 /// - `Option<&[VirtualNode]>` - The children, or `None`.
291 pub fn try_get_children(&self) -> Option<&[VirtualNode]> {
292 match self {
293 Self::Element { children, .. } => Some(children.as_slice()),
294 Self::Fragment(children) => Some(children.as_slice()),
295 _ => None,
296 }
297 }
298
299 /// Extends this node's attribute list with the given entries, then
300 /// returns the node. If the node is not an `Element` variant, the
301 /// entries are dropped and the node is returned unchanged.
302 ///
303 /// Used by the `html!` macro to splice `class` / `style` / event
304 /// handler attributes onto a component-returned node without forcing
305 /// a `let mut` binding in the generated code.
306 ///
307 /// # Arguments
308 ///
309 /// - `I: IntoIterator<Item = AttributeEntry>` - The extra entries to append.
310 ///
311 /// # Returns
312 ///
313 /// - `Self` - The node with extended attributes (or unchanged).
314 pub fn extend_attributes<I>(self, extra: I) -> Self
315 where
316 I: IntoIterator<Item = AttributeEntry>,
317 {
318 match self {
319 Self::Element {
320 tag,
321 attributes,
322 children,
323 key,
324 props,
325 } => {
326 let mut attrs: Vec<AttributeEntry> = attributes;
327 attrs.extend(extra);
328 Self::Element {
329 tag,
330 attributes: attrs,
331 children,
332 key,
333 props,
334 }
335 }
336 other => other,
337 }
338 }
339
340 /// Returns the diffing key of this node, if it has one.
341 ///
342 /// Recognizes keys on `Element` variants. Other variants
343 /// (`Text`, `Fragment`, `Dynamic`, `Empty`) do not have keys.
344 /// This matches the renderer's `get_node_key` semantics
345 /// in `core/src/renderer/render/impl.rs`.
346 ///
347 /// # Returns
348 ///
349 /// - `Option<&str>` - The key, or `None` if this node has no key
350 /// or is not an `Element` variant.
351 pub fn key(&self) -> Option<&str> {
352 match self {
353 Self::Element { key, .. } => key.as_deref(),
354 _ => None,
355 }
356 }
357
358 /// Returns `true` if this node has a non-`None` diffing key.
359 ///
360 /// # Returns
361 ///
362 /// - `bool` - `true` if `key()` returns `Some`, `false` otherwise.
363 pub fn has_key(&self) -> bool {
364 self.key().is_some()
365 }
366}
367
368/// Implementation of virtual node construction for `VirtualNode<()>`.
369impl VirtualNode<()> {
370 /// Creates a dynamic node with the given render function.
371 ///
372 /// # Arguments
373 ///
374 /// - `F: FnMut(&mut HookContext) -> Self + 'static` - The render function.
375 ///
376 /// # Returns
377 ///
378 /// - `Self` - The dynamic node.
379 pub fn create_dynamic<F>(render_fn: F) -> Self
380 where
381 F: FnMut(&mut HookContext) -> Self + 'static,
382 {
383 let hook_context: HookContext = HookContext::default();
384 let inner: Rc<UnsafeCell<RenderFnInner>> =
385 Rc::new(UnsafeCell::new(RenderFnInner::new(Box::new(render_fn))));
386 Self::Dynamic(DynamicNode::new(inner, hook_context))
387 }
388}