Skip to main content

euv_core/vdom/
impl.rs

1use crate::*;
2
3/// Maps each `Attribute` variant to its corresponding DOM attribute string.
4impl Attribute {
5    /// Returns the string representation of this attribute name for DOM binding.
6    pub fn as_str(&self) -> String {
7        match self {
8            Attribute::AccessKey => "accesskey".to_string(),
9            Attribute::Action => "action".to_string(),
10            Attribute::Alt => "alt".to_string(),
11            Attribute::AriaLabel => "aria-label".to_string(),
12            Attribute::AutoComplete => "autocomplete".to_string(),
13            Attribute::AutoFocus => "autofocus".to_string(),
14            Attribute::Checked => "checked".to_string(),
15            Attribute::Class => "class".to_string(),
16            Attribute::Cols => "cols".to_string(),
17            Attribute::ContentEditable => "contenteditable".to_string(),
18            Attribute::Data(name) => format!("data-{}", name),
19            Attribute::Dir => "dir".to_string(),
20            Attribute::Disabled => "disabled".to_string(),
21            Attribute::Draggable => "draggable".to_string(),
22            Attribute::EncType => "enctype".to_string(),
23            Attribute::For => "for".to_string(),
24            Attribute::Form => "form".to_string(),
25            Attribute::Height => "height".to_string(),
26            Attribute::Hidden => "hidden".to_string(),
27            Attribute::Href => "href".to_string(),
28            Attribute::Id => "id".to_string(),
29            Attribute::Lang => "lang".to_string(),
30            Attribute::Max => "max".to_string(),
31            Attribute::MaxLength => "maxlength".to_string(),
32            Attribute::Method => "method".to_string(),
33            Attribute::Min => "min".to_string(),
34            Attribute::MinLength => "minlength".to_string(),
35            Attribute::Multiple => "multiple".to_string(),
36            Attribute::Name => "name".to_string(),
37            Attribute::Pattern => "pattern".to_string(),
38            Attribute::Placeholder => "placeholder".to_string(),
39            Attribute::ReadOnly => "readonly".to_string(),
40            Attribute::Required => "required".to_string(),
41            Attribute::Rows => "rows".to_string(),
42            Attribute::Selected => "selected".to_string(),
43            Attribute::Size => "size".to_string(),
44            Attribute::SpellCheck => "spellcheck".to_string(),
45            Attribute::Src => "src".to_string(),
46            Attribute::Step => "step".to_string(),
47            Attribute::Style => "style".to_string(),
48            Attribute::TabIndex => "tabindex".to_string(),
49            Attribute::Target => "target".to_string(),
50            Attribute::Title => "title".to_string(),
51            Attribute::Type => "type".to_string(),
52            Attribute::Value => "value".to_string(),
53            Attribute::Width => "width".to_string(),
54            Attribute::Other(name) => name.clone(),
55        }
56    }
57}
58
59/// Clones a `DynamicNode` by cloning its `HookContext` (Copy) and `render_fn` (Rc).
60impl Clone for DynamicNode {
61    fn clone(&self) -> Self {
62        DynamicNode {
63            render_fn: Rc::clone(&self.render_fn),
64            hook_context: self.hook_context,
65        }
66    }
67}
68
69/// Converts a `VirtualNode` reference into an owned node.
70impl AsNode for VirtualNode {
71    fn as_node(&self) -> Option<VirtualNode> {
72        Some(self.clone())
73    }
74}
75
76/// Converts a `VirtualNode` reference into an owned node.
77impl AsNode for &VirtualNode {
78    fn as_node(&self) -> Option<VirtualNode> {
79        Some((*self).clone())
80    }
81}
82
83/// Converts a `String` into a text virtual node.
84impl AsNode for String {
85    fn as_node(&self) -> Option<VirtualNode> {
86        Some(VirtualNode::Text(TextNode::new(self.clone(), None)))
87    }
88}
89
90/// Converts a string slice into a text virtual node.
91impl AsNode for &str {
92    fn as_node(&self) -> Option<VirtualNode> {
93        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
94    }
95}
96
97/// Converts an `i32` into a text virtual node.
98impl AsNode for i32 {
99    fn as_node(&self) -> Option<VirtualNode> {
100        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
101    }
102}
103
104/// Converts an `i64` into a text virtual node.
105impl AsNode for i64 {
106    fn as_node(&self) -> Option<VirtualNode> {
107        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
108    }
109}
110
111/// Converts a `usize` into a text virtual node.
112impl AsNode for usize {
113    fn as_node(&self) -> Option<VirtualNode> {
114        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
115    }
116}
117
118/// Converts an `f32` into a text virtual node.
119impl AsNode for f32 {
120    fn as_node(&self) -> Option<VirtualNode> {
121        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
122    }
123}
124
125/// Converts an `f64` into a text virtual node.
126impl AsNode for f64 {
127    fn as_node(&self) -> Option<VirtualNode> {
128        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
129    }
130}
131
132/// Converts a `bool` into a text virtual node.
133impl AsNode for bool {
134    fn as_node(&self) -> Option<VirtualNode> {
135        Some(VirtualNode::Text(TextNode::new(self.to_string(), None)))
136    }
137}
138
139/// Converts a signal into a reactive text virtual node.
140impl<T> AsNode for Signal<T>
141where
142    T: Clone + PartialEq + std::fmt::Display + 'static,
143{
144    fn as_node(&self) -> Option<VirtualNode> {
145        Some(self.as_reactive_text())
146    }
147}
148
149/// Converts a `VirtualNode` into itself via `IntoNode`.
150impl IntoNode for VirtualNode {
151    fn into_node(self) -> VirtualNode {
152        self
153    }
154}
155
156/// Wraps a `FnMut() -> VirtualNode` closure into a `DynamicNode` via `IntoNode`.
157///
158/// This enables writing `{move || rsx! { ... }}` directly in RSX markup
159/// without explicit `DynamicNode` construction.
160impl<F> IntoNode for F
161where
162    F: FnMut() -> VirtualNode + 'static,
163{
164    fn into_node(self) -> VirtualNode {
165        VirtualNode::Dynamic(DynamicNode {
166            render_fn: Rc::new(RefCell::new(self)),
167            hook_context: crate::reactive::create_hook_context(),
168        })
169    }
170}
171
172/// Converts a `String` into a text virtual node via `IntoNode`.
173impl IntoNode for String {
174    fn into_node(self) -> VirtualNode {
175        VirtualNode::Text(TextNode::new(self, None))
176    }
177}
178
179/// Converts a `&str` into a text virtual node via `IntoNode`.
180impl IntoNode for &str {
181    fn into_node(self) -> VirtualNode {
182        VirtualNode::Text(TextNode::new(self.to_string(), None))
183    }
184}
185
186/// Converts an `i32` into a text virtual node via `IntoNode`.
187impl IntoNode for i32 {
188    fn into_node(self) -> VirtualNode {
189        VirtualNode::Text(TextNode::new(self.to_string(), None))
190    }
191}
192
193/// Converts a `usize` into a text virtual node via `IntoNode`.
194impl IntoNode for usize {
195    fn into_node(self) -> VirtualNode {
196        VirtualNode::Text(TextNode::new(self.to_string(), None))
197    }
198}
199
200/// Converts a `bool` into a text virtual node via `IntoNode`.
201impl IntoNode for bool {
202    fn into_node(self) -> VirtualNode {
203        VirtualNode::Text(TextNode::new(self.to_string(), None))
204    }
205}
206
207/// Converts a signal into a reactive text virtual node via `IntoNode`.
208impl<T> IntoNode for Signal<T>
209where
210    T: Clone + PartialEq + std::fmt::Display + 'static,
211{
212    fn into_node(self) -> VirtualNode {
213        self.as_reactive_text()
214    }
215}
216
217/// Implementation of virtual node construction and property extraction.
218impl VirtualNode {
219    /// Creates a new element node with the given tag name.
220    pub fn get_element_node(tag_name: &str) -> Self {
221        VirtualNode::Element {
222            tag: Tag::Element(tag_name.to_string()),
223            attributes: Vec::new(),
224            children: Vec::new(),
225            key: None,
226        }
227    }
228
229    /// Creates a new text node with the given content.
230    pub fn get_text_node(content: &str) -> Self {
231        VirtualNode::Text(TextNode::new(content.to_string(), None))
232    }
233
234    /// Adds an attribute to this node if it is an element.
235    pub fn with_attribute(mut self, name: &str, value: AttributeValue) -> Self {
236        if let VirtualNode::Element {
237            ref mut attributes, ..
238        } = self
239        {
240            attributes.push(AttributeEntry::new(name.to_string(), value));
241        }
242        self
243    }
244
245    /// Adds a child node to this node if it is an element.
246    pub fn with_child(mut self, child: VirtualNode) -> Self {
247        if let VirtualNode::Element {
248            ref mut children, ..
249        } = self
250        {
251            children.push(child);
252        }
253        self
254    }
255
256    /// Returns true if this node is a component node.
257    pub fn is_component(&self) -> bool {
258        matches!(
259            self,
260            VirtualNode::Element {
261                tag: Tag::Component(_),
262                ..
263            }
264        )
265    }
266
267    /// Returns the tag name if this is an element or component node.
268    pub fn tag_name(&self) -> Option<String> {
269        match self {
270            VirtualNode::Element { tag, .. } => match tag {
271                Tag::Element(name) => Some(name.clone()),
272                Tag::Component(name) => Some(name.clone()),
273            },
274            _ => None,
275        }
276    }
277
278    /// Extracts a string property from this node if it is an element with the named attribute.
279    pub fn try_get_prop(&self, name: &Attribute) -> Option<String> {
280        let name_str: String = name.as_str();
281        if let VirtualNode::Element { attributes, .. } = self {
282            for attr in attributes {
283                if attr.get_name() == &name_str {
284                    match attr.get_value() {
285                        AttributeValue::Text(value) => return Some(value.clone()),
286                        AttributeValue::Signal(signal) => return Some(signal.get()),
287                        _ => {}
288                    }
289                }
290            }
291        }
292        None
293    }
294
295    /// Extracts children from this node if it is an element.
296    pub fn get_children(&self) -> Vec<VirtualNode> {
297        if let VirtualNode::Element { children, .. } = self {
298            children.clone()
299        } else {
300            Vec::new()
301        }
302    }
303
304    /// Extracts text content from this node.
305    pub fn try_get_text(&self) -> Option<String> {
306        match self {
307            VirtualNode::Text(text_node) => Some(text_node.get_content().clone()),
308            VirtualNode::Element { children, .. } => {
309                children.first().and_then(VirtualNode::try_get_text)
310            }
311            _ => None,
312        }
313    }
314
315    /// Extracts an event handler from this node if it is an element with the named event attribute.
316    pub fn try_get_event(
317        &self,
318        name: &NativeEventName,
319    ) -> Option<crate::event::NativeEventHandler> {
320        let name_str: String = name.as_str();
321        if let VirtualNode::Element { attributes, .. } = self {
322            for attr in attributes {
323                if attr.get_name() == &name_str
324                    && let AttributeValue::Event(handler) = attr.get_value()
325                {
326                    return Some(handler.clone());
327                }
328            }
329        }
330        None
331    }
332
333    /// Extracts an event handler from this node by a custom attribute name.
334    pub fn try_get_callback(&self, name: &str) -> Option<crate::event::NativeEventHandler> {
335        if let VirtualNode::Element { attributes, .. } = self {
336            for attr in attributes {
337                if attr.get_name() == name
338                    && let AttributeValue::Event(handler) = attr.get_value()
339                {
340                    return Some(handler.clone());
341                }
342            }
343        }
344        None
345    }
346}
347
348/// Converts a signal into a reactive text node with listener wiring.
349impl<T> AsReactiveText for Signal<T>
350where
351    T: Clone + PartialEq + std::fmt::Display + 'static,
352{
353    fn as_reactive_text(&self) -> VirtualNode {
354        let signal: Signal<T> = *self;
355        let initial: String = signal.get().to_string();
356        let string_signal: Signal<String> = {
357            let boxed: Box<SignalInner<String>> = Box::new(SignalInner::new(initial.clone()));
358            Signal::from_inner(Box::leak(boxed) as *mut SignalInner<String>)
359        };
360        let source_signal: Signal<T> = *self;
361        let string_signal_clone: Signal<String> = string_signal;
362        source_signal.subscribe({
363            let source_signal: Signal<T> = source_signal;
364            move || {
365                let new_value: String = source_signal.get().to_string();
366                string_signal_clone.set(new_value);
367            }
368        });
369        VirtualNode::Text(TextNode::new(initial, Some(string_signal)))
370    }
371}
372
373/// Implementation of style CSS serialization.
374impl Style {
375    /// Adds a style property.
376    ///
377    /// Property names are automatically converted from snake_case to kebab-case
378    /// (e.g., `flex_direction` becomes `flex-direction`).
379    pub fn property<N, V>(mut self, name: N, value: V) -> Self
380    where
381        N: AsRef<str>,
382        V: AsRef<str>,
383    {
384        self.get_mut_properties().push(StyleProperty::new(
385            name.as_ref().replace('_', "-"),
386            value.as_ref().to_string(),
387        ));
388        self
389    }
390
391    /// Converts the style to a CSS string.
392    pub fn to_css_string(&self) -> String {
393        self.get_properties()
394            .iter()
395            .map(|p| format!("{}: {};", p.get_name(), p.get_value()))
396            .collect::<Vec<String>>()
397            .join(" ")
398    }
399}
400
401/// Provides a default empty style.
402impl Default for Style {
403    fn default() -> Self {
404        Self::new(Vec::new())
405    }
406}
407
408/// Implementation of StyleProperty construction.
409impl StyleProperty {
410    /// Creates a new style property with the given name and value.
411    pub fn new(name: String, value: String) -> Self {
412        let mut prop: StyleProperty = StyleProperty::default();
413        prop.set_name(name);
414        prop.set_value(value);
415        prop
416    }
417}
418
419/// Implementation of CssClass construction and style injection.
420impl CssClass {
421    /// Creates a new CSS class with the given name and style declarations.
422    pub fn new(name: String, style: String) -> Self {
423        let mut css_class: CssClass = CssClass::default();
424        css_class.set_name(name);
425        css_class.set_style(style);
426        css_class
427    }
428
429    /// Injects this class's styles into the DOM if not already present.
430    ///
431    /// Creates a `<style>` element with id `euv-css-injected` on first call,
432    /// then appends the class rule. Subsequent calls for the same class name
433    /// are no-ops. On first creation, also injects global CSS keyframes
434    /// required by built-in animations.
435    pub fn inject_style(&self) {
436        #[cfg(target_arch = "wasm32")]
437        {
438            let style_id: &str = "euv-css-injected";
439            let document: web_sys::Document = web_sys::window()
440                .expect("no global window exists")
441                .document()
442                .expect("no document exists");
443            let style_element: web_sys::HtmlStyleElement = match document
444                .get_element_by_id(style_id)
445            {
446                Some(el) => el.dyn_into::<web_sys::HtmlStyleElement>().unwrap(),
447                None => {
448                    let el: web_sys::HtmlStyleElement = document
449                        .create_element("style")
450                        .unwrap()
451                        .dyn_into::<web_sys::HtmlStyleElement>()
452                        .unwrap();
453                    el.set_id(style_id);
454                    let keyframes: &str = "@keyframes euv-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }";
455                    el.set_inner_text(keyframes);
456                    document.head().unwrap().append_child(&el).unwrap();
457                    el
458                }
459            };
460            let existing_css: String = style_element.inner_text();
461            let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
462            if !existing_css.contains(&class_rule) {
463                let new_css: String = if existing_css.is_empty() {
464                    class_rule
465                } else {
466                    format!("{}\n{}", existing_css, class_rule)
467                };
468                style_element.set_inner_text(&new_css);
469            }
470        }
471    }
472}