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