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