Skip to main content

euv_core/vdom/attribute/
impl.rs

1use crate::*;
2
3/// SAFETY: `InjectedClassesCell` is only used in single-threaded WASM contexts.
4unsafe impl Sync for InjectedClassesCell {}
5
6/// Implementation of attribute value factory methods for reactive and merged values.
7impl AttributeValue {
8    /// Creates a reactive attribute `Self` for conditional attribute values.
9    ///
10    /// This function replaces the inline `Signal::create(...)` + `subscribe_attr_signal(...)`
11    /// boilerplate that was previously generated by the `html!` macro for every
12    /// attribute value containing an `if` condition.
13    ///
14    /// # Arguments
15    ///
16    /// - `Fn() -> String + 'static` - A closure that computes the current attribute value.
17    ///   Called on initial render and whenever any signal changes.
18    ///
19    /// # Returns
20    ///
21    /// - `Self` - A `Self::Signal` backed by a `Signal<String>`
22    ///   that reactively re-evaluates the attribute value on signal updates.
23    pub fn create_reactive_signal<F>(compute: F) -> Self
24    where
25        F: Fn() -> String + 'static,
26    {
27        let attr_signal: Signal<String> =
28            Signal::create(IntoReactiveString::into_reactive_string(compute()));
29        subscribe_attr_signal(attr_signal, move || {
30            IntoReactiveString::into_reactive_string(compute())
31        });
32        Self::Signal(attr_signal)
33    }
34
35    /// Merges multiple class attribute values into a single `Self`.
36    ///
37    /// Each input value is adapted into a `Self` via `IntoReactiveValue`.
38    /// `Css` values are injected into the DOM and their names are collected.
39    /// All non-empty class names are joined with spaces into a final `Text` attribute.
40    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute
41    /// that re-evaluates when any constituent signal changes.
42    ///
43    /// # Arguments
44    ///
45    /// - `&[Self]` - The class attribute values to merge.
46    ///
47    /// # Returns
48    ///
49    /// - `Self` - A merged attribute value containing space-separated class names.
50    pub fn merge_class(values: &[Self]) -> Self {
51        let has_signal: bool = values
52            .iter()
53            .any(|value: &Self| matches!(value, Self::Signal(_)));
54        if has_signal {
55            let owned_values: Vec<Self> = values.to_vec();
56            let compute = move || {
57                let mut result: String = String::new();
58                for value in &owned_values {
59                    let class_segment: String = match value {
60                        Self::Css(css) => {
61                            css.inject_style();
62                            css.get_name().to_string()
63                        }
64                        Self::Text(text_value) => text_value.clone(),
65                        Self::Signal(signal) => signal.get(),
66                        _ => String::new(),
67                    };
68                    if !class_segment.is_empty() {
69                        if !result.is_empty() {
70                            result.push(CHAR_SPACE);
71                        }
72                        result.push_str(&class_segment);
73                    }
74                }
75                result
76            };
77            let attr_signal: Signal<String> = Signal::create(compute());
78            subscribe_attr_signal(attr_signal, compute);
79            Self::Signal(attr_signal)
80        } else {
81            let mut result: String = String::new();
82            for value in values {
83                let class_segment: String = match value {
84                    Self::Css(css) => {
85                        css.inject_style();
86                        css.get_name().to_string()
87                    }
88                    Self::Text(text_value) => text_value.clone(),
89                    _ => String::new(),
90                };
91                if !class_segment.is_empty() {
92                    if !result.is_empty() {
93                        result.push(CHAR_SPACE);
94                    }
95                    result.push_str(&class_segment);
96                }
97            }
98            Self::Text(result)
99        }
100    }
101
102    /// Merges multiple style attribute values into a single `Self`.
103    ///
104    /// Each input value is expected to be a style string (`Text`) or a reactive
105    /// `Signal<String>` producing a style string. All non-empty style strings are
106    /// joined with spaces into a final combined style attribute.
107    /// If any value is signal-backed, the result becomes a reactive `Signal` attribute.
108    ///
109    /// # Arguments
110    ///
111    /// - `&[Self]` - The style attribute values to merge.
112    ///
113    /// # Returns
114    ///
115    /// - `Self` - A merged attribute value containing the combined CSS style string.
116    pub fn merge_style(values: &[Self]) -> Self {
117        let has_signal: bool = values
118            .iter()
119            .any(|value: &Self| matches!(value, Self::Signal(_)));
120        if has_signal {
121            let owned_values: Vec<Self> = values.to_vec();
122            let compute = move || {
123                let mut result: String = String::new();
124                for value in &owned_values {
125                    let style_segment: String = match value {
126                        Self::Text(text_value) => text_value.clone(),
127                        Self::Signal(signal) => signal.get(),
128                        _ => String::new(),
129                    };
130                    if !style_segment.is_empty() {
131                        if !result.is_empty() {
132                            result.push(CHAR_SPACE);
133                        }
134                        result.push_str(&style_segment);
135                    }
136                }
137                result
138            };
139            let attr_signal: Signal<String> = Signal::create(compute());
140            subscribe_attr_signal(attr_signal, compute);
141            Self::Signal(attr_signal)
142        } else {
143            let mut result: String = String::new();
144            for value in values {
145                let style_segment: String = match value {
146                    Self::Text(text_value) => text_value.clone(),
147                    _ => String::new(),
148                };
149                if !style_segment.is_empty() {
150                    if !result.is_empty() {
151                        result.push(CHAR_SPACE);
152                    }
153                    result.push_str(&style_segment);
154                }
155            }
156            Self::Text(result)
157        }
158    }
159}
160
161/// Visual equality comparison for attribute values.
162///
163/// Compares values by their visual output rather than identity. `Signal`
164/// values are compared by their current resolved string; when both signals
165/// share the same inner pointer, they are always considered **unequal**
166/// because the signal may have mutated between VDOM snapshots and `.get()`
167/// would return the same current value for both, masking the change.
168/// `Event` values are always considered equal (re-binding is handled by the
169/// handler registry), and `Css` values are compared by class name.
170impl PartialEq for AttributeValue {
171    /// Compares two attribute values for visual equality.
172    ///
173    /// # Arguments
174    ///
175    /// - `&Self` - The first attribute value.
176    /// - `&Self` - The second attribute value.
177    ///
178    /// # Returns
179    ///
180    /// - `bool` - `true` if the values are visually equal.
181    fn eq(&self, other: &Self) -> bool {
182        match (self, other) {
183            (Self::Text(old_value), Self::Text(new_value)) => old_value == new_value,
184            (Self::Signal(old_signal), Self::Signal(new_signal)) => {
185                if old_signal.get_inner_addr() == new_signal.get_inner_addr() {
186                    return false;
187                }
188                old_signal.get() == new_signal.get()
189            }
190            (Self::Signal(old_signal), Self::Text(new_value)) => old_signal.get() == *new_value,
191            (Self::Text(old_value), Self::Signal(new_signal)) => *old_value == new_signal.get(),
192            (Self::Event(_), Self::Event(_)) => true,
193            (Self::Css(old_class), Self::Css(new_class)) => {
194                old_class.get_name() == new_class.get_name()
195            }
196            (Self::Dynamic(old_dynamic), Self::Dynamic(new_dynamic)) => old_dynamic == new_dynamic,
197            _ => false,
198        }
199    }
200}
201
202/// Visual equality comparison for attribute entries.
203///
204/// Two attribute entries are equal when their names match and their values
205/// are visually equal as defined by `AttributeValue::eq`.
206impl PartialEq for AttributeEntry {
207    /// Compares two attribute entries for visual equality.
208    ///
209    /// # Arguments
210    ///
211    /// - `&Self` - The first attribute entry.
212    /// - `&Self` - The second attribute entry.
213    ///
214    /// # Returns
215    ///
216    /// - `bool` - `true` if both names and values match.
217    fn eq(&self, other: &Self) -> bool {
218        self.get_name() == other.get_name() && self.get_value() == other.get_value()
219    }
220}
221
222/// Visual equality comparison for CSS classes.
223///
224/// Two CSS classes are considered equal when their class names match,
225/// since the name uniquely identifies the visual style rule.
226impl PartialEq for Css {
227    /// Compares two CSS classes by name.
228    ///
229    /// # Arguments
230    ///
231    /// - `&Self` - The first CSS class.
232    /// - `&Self` - The second CSS class.
233    ///
234    /// # Returns
235    ///
236    /// - `bool` - `true` if the class names match.
237    fn eq(&self, other: &Self) -> bool {
238        self.get_name() == other.get_name()
239    }
240}
241
242/// Implementation of style CSS serialization.
243impl Style {
244    /// Adds a style property.
245    ///
246    /// Property names are automatically converted from snake_case to kebab-case
247    /// (e.g., `flex_direction` becomes `flex-direction`).
248    ///
249    /// # Arguments
250    ///
251    /// - `N` - The property name (snake_case will be converted to kebab-case).
252    /// - `V` - The property value.
253    ///
254    /// # Returns
255    ///
256    /// - `Self` - This style with the property added.
257    pub fn property<N, V>(mut self, name: N, value: V) -> Self
258    where
259        N: AsRef<str>,
260        V: AsRef<str>,
261    {
262        self.get_mut_properties().push(StyleProperty::new(
263            name.as_ref().replace(CHAR_UNDERSCORE, STR_HYPHEN),
264            value.as_ref().to_string(),
265        ));
266        self
267    }
268
269    /// Converts the style to a CSS string.
270    ///
271    /// # Returns
272    ///
273    /// - `String` - The CSS string representation.
274    pub fn to_css_string(&self) -> String {
275        self.get_properties()
276            .iter()
277            .map(|style: &StyleProperty| {
278                format!(
279                    "{name}{CSS_PROP_SEPARATOR}{value}{CHAR_CSS_DECL_TERMINATOR}",
280                    name = style.get_name(),
281                    value = style.get_value()
282                )
283            })
284            .collect::<Vec<String>>()
285            .join(" ")
286    }
287
288    /// Builds a CSS style string from an array of key-value pairs.
289    ///
290    /// This function is used by the `html!` macro to convert static `style:`
291    /// attributes into a CSS string without allocating intermediate `Style`
292    /// and `Vec<StyleProperty>` objects. Keys are converted from snake_case
293    /// to kebab-case automatically.
294    ///
295    /// # Arguments
296    ///
297    /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
298    ///
299    /// # Returns
300    ///
301    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
302    pub fn create_style_string(props: &[(&str, &str)]) -> String {
303        let mut result: String = String::new();
304        for (key, value) in props {
305            if !result.is_empty() {
306                result.push(CHAR_SPACE);
307            }
308            result.push_str(&key.replace(CHAR_UNDERSCORE, STR_HYPHEN));
309            result.push_str(CSS_PROP_SEPARATOR);
310            result.push_str(value);
311            result.push(CHAR_CSS_DECL_TERMINATOR);
312        }
313        result
314    }
315}
316
317/// Provides a default empty style.
318impl Default for Style {
319    /// Returns a default `Self` with no properties.
320    ///
321    /// # Returns
322    ///
323    /// - `Self` - An empty style.
324    fn default() -> Self {
325        Self::new(Vec::new())
326    }
327}
328
329/// Implementation of Css construction and style injection.
330impl Css {
331    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
332    ///
333    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
334    /// This is used by the `class!` macro for fully static class definitions
335    /// where pseudo rules can be computed at compile time.
336    ///
337    /// # Arguments
338    ///
339    /// - `&str` - The serialized pseudo rules string.
340    ///
341    /// # Returns
342    ///
343    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
344    pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
345        let mut rules: Vec<PseudoRule> = Vec::new();
346        let mut remaining: &str = input;
347        while !remaining.is_empty() {
348            let selector_end: Option<usize> = remaining.find(CSS_RULE_OPEN);
349            let Some(selector_end_index) = selector_end else {
350                break;
351            };
352            let selector: &str = &remaining[..selector_end_index];
353            let after_selector: &str = remaining[selector_end_index..]
354                .strip_prefix(CSS_RULE_OPEN)
355                .unwrap_or_default();
356            let style_end: Option<usize> = after_selector.find(CHAR_CSS_RULE_CLOSE);
357            let Some(style_end_index) = style_end else {
358                break;
359            };
360            let style: &str = &after_selector[..style_end_index];
361            if !selector.is_empty() && !style.is_empty() {
362                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
363            }
364            remaining = after_selector[style_end_index..]
365                .strip_prefix(CHAR_CSS_RULE_CLOSE)
366                .unwrap_or_default();
367        }
368        rules
369    }
370
371    /// Parses media query rules from a compact serialization string.
372    ///
373    /// The serialization format is: `@media query { key: value; key: value; }@media query2 { ... }`
374    /// This is used by the `class!` macro for fully static class definitions
375    /// where media rules can be computed at compile time.
376    ///
377    /// # Arguments
378    ///
379    /// - `&str` - The serialized media rules string.
380    ///
381    /// # Returns
382    ///
383    /// - `Vec<MediaRule>` - The parsed media rules.
384    pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
385        let mut rules: Vec<MediaRule> = Vec::new();
386        let mut remaining: &str = input;
387        while !remaining.is_empty() {
388            if !remaining.starts_with(CSS_MEDIA_PREFIX) {
389                break;
390            }
391            let after_prefix: &str = remaining.strip_prefix(CSS_MEDIA_PREFIX).unwrap_or_default();
392            let query_end: Option<usize> = after_prefix.find(CSS_RULE_OPEN);
393            let Some(query_end_index) = query_end else {
394                break;
395            };
396            let query: &str = &after_prefix[..query_end_index];
397            let after_query: &str = after_prefix[query_end_index..]
398                .strip_prefix(CSS_RULE_OPEN)
399                .unwrap_or_default();
400            let style_end: Option<usize> = after_query.find(CHAR_CSS_RULE_CLOSE);
401            let Some(style_end_index) = style_end else {
402                break;
403            };
404            let style: &str = &after_query[..style_end_index];
405            if !query.is_empty() && !style.is_empty() {
406                rules.push(MediaRule::new(query.to_string(), style.to_string()));
407            }
408            remaining = after_query[style_end_index..]
409                .strip_prefix(CHAR_CSS_RULE_CLOSE)
410                .unwrap_or_default();
411        }
412        rules
413    }
414
415    /// Injects this class's styles into the DOM if not already present.
416    ///
417    /// Uses a global `HashSet` to track injected class names, avoiding the
418    /// expensive `existing_css.contains(css)` full-text search on every call.
419    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
420    /// then appends them directly to the `<style>` element via
421    /// `append_child` with a new text node — no read-modify-write of the
422    /// entire stylesheet content.
423    ///
424    /// # Panics
425    ///
426    /// Panics if `window()` or `document()` is unavailable on the current platform.
427    pub fn inject_style(&self) {
428        if !Self::mark_injected(self.get_name().clone()) {
429            return;
430        }
431        let class_rule: String = format!(
432            "{CHAR_CSS_CLASS_PREFIX}{} {{ {} }}",
433            self.get_name(),
434            self.get_style()
435        );
436        let mut css_text: String = class_rule;
437        for pseudo_rule in self.get_pseudo_rules() {
438            if !pseudo_rule.get_style().is_empty() {
439                let pseudo_rule_str: String = format!(
440                    "{CHAR_CSS_CLASS_PREFIX}{}{} {{ {} }}",
441                    self.get_name(),
442                    pseudo_rule.get_selector(),
443                    pseudo_rule.get_style()
444                );
445                css_text = format!("{css_text}\n{pseudo_rule_str}");
446            }
447        }
448        for media_rule in self.get_media_rules() {
449            if !media_rule.get_query().is_empty() {
450                let media_rule_str: String = format!(
451                    "@media {} {{ {CHAR_CSS_CLASS_PREFIX}{} {{ {} }} }}",
452                    media_rule.get_query(),
453                    self.get_name(),
454                    media_rule.get_style()
455                );
456                css_text = format!("{css_text}\n{media_rule_str}");
457            }
458        }
459        Self::append_css(&css_text);
460    }
461
462    /// Marks a class name as injected in the global `HashSet`.
463    ///
464    /// Returns `false` if the class was already injected (no-op), `true`
465    /// if this is the first injection.
466    ///
467    /// # Arguments
468    ///
469    /// - `String` - The class name to mark as injected.
470    ///
471    /// # Returns
472    ///
473    /// - `bool` - `true` if newly injected, `false` if already present.
474    fn mark_injected(class_name: String) -> bool {
475        get_injected_classes_mut().insert(class_name)
476    }
477
478    /// Appends CSS text directly to the shared `<style>` element.
479    ///
480    /// Creates a new text node and appends it as a child of the `<style>`
481    /// element, avoiding the read-modify-write pattern of reading the entire
482    /// `innerText`, concatenating, and setting it back.
483    ///
484    /// # Arguments
485    ///
486    /// - `&str` - The CSS text to append.
487    ///
488    fn append_css(css_text: &str) {
489        let style_id: &str = EUV_CSS_INJECTED_ID;
490        let window_value: Window = match window() {
491            Some(w) => w,
492            None => return,
493        };
494        let document: Document = match window_value.document() {
495            Some(d) => d,
496            None => return,
497        };
498        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
499            Some(existing_element) => match existing_element.dyn_into::<HtmlStyleElement>() {
500                Ok(el) => el,
501                Err(_err) => return,
502            },
503            None => {
504                let created: Element = match document.create_element(STYLE_TAG) {
505                    Ok(el) => el,
506                    Err(_err) => return,
507                };
508                let style_element_from_id: HtmlStyleElement =
509                    match created.dyn_into::<HtmlStyleElement>() {
510                        Ok(el) => el,
511                        Err(_err) => return,
512                    };
513                style_element_from_id.set_id(style_id);
514                if let Some(head) = document.head() {
515                    let _ = head.append_child(&style_element_from_id);
516                }
517                style_element_from_id
518            }
519        };
520        if !css_text.is_empty() {
521            let text_node: Text = document.create_text_node(css_text);
522            let _ = style_element.append_child(&text_node);
523        }
524    }
525
526    /// Injects CSS text into the shared `<style>` element in the DOM.
527    ///
528    /// Delegates to [`Css::append_css`] for the actual DOM append.
529    /// Unlike the previous implementation, this does not read the existing
530    /// stylesheet content or perform a full-text `contains` search.
531    ///
532    /// # Arguments
533    ///
534    /// - `&str` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
535    ///
536    /// # Panics
537    ///
538    /// Panics if `window()` or `document()` is unavailable on the current platform.
539    pub fn inject_css(css_text: &str) {
540        Self::append_css(css_text);
541    }
542}
543
544/// Displays the CSS class name.
545///
546/// This enables `format!("{}", css)` to produce the class name string,
547/// which is required for reactive `if` conditions in `class:` attributes.
548impl Display for Css {
549    /// Formats the CSS class as its name string.
550    ///
551    /// # Arguments
552    ///
553    /// - `&mut Formatter` - The formatter.
554    ///
555    /// # Returns
556    ///
557    /// - `fmt::Result` - The formatting result.
558    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
559        write!(formatter, "{class_name}", class_name = self.get_name())
560    }
561}