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/// Visual equality comparison for attribute values.
7///
8/// Compares values by their visual output rather than identity. `Signal`
9/// values are compared by their current resolved string; when both signals
10/// share the same inner pointer, they are always considered **unequal**
11/// because the signal may have mutated between VDOM snapshots and `.get()`
12/// would return the same current value for both, masking the change.
13/// `Event` values are always considered equal (re-binding is handled by the
14/// handler registry), and `CssClass` values are compared by class name.
15impl PartialEq for AttributeValue {
16    /// Compares two attribute values for visual equality.
17    ///
18    /// # Arguments
19    ///
20    /// - `&Self` - The first attribute value.
21    /// - `&Self` - The second attribute value.
22    ///
23    /// # Returns
24    ///
25    /// - `bool` - `true` if the values are visually equal.
26    fn eq(&self, other: &Self) -> bool {
27        match (self, other) {
28            (AttributeValue::Text(old_val), AttributeValue::Text(new_val)) => old_val == new_val,
29            (AttributeValue::Signal(old_sig), AttributeValue::Signal(new_sig)) => {
30                if old_sig.get_inner_addr() == new_sig.get_inner_addr() {
31                    return false;
32                }
33                old_sig.get() == new_sig.get()
34            }
35            (AttributeValue::Signal(old_sig), AttributeValue::Text(new_val)) => {
36                old_sig.get() == *new_val
37            }
38            (AttributeValue::Text(old_val), AttributeValue::Signal(new_sig)) => {
39                *old_val == new_sig.get()
40            }
41            (AttributeValue::Event(_), AttributeValue::Event(_)) => true,
42            (AttributeValue::Css(old_css), AttributeValue::Css(new_css)) => {
43                old_css.get_name() == new_css.get_name()
44            }
45            (AttributeValue::Dynamic(old_dyn), AttributeValue::Dynamic(new_dyn)) => {
46                old_dyn == new_dyn
47            }
48            _ => false,
49        }
50    }
51}
52
53/// Visual equality comparison for attribute entries.
54///
55/// Two attribute entries are equal when their names match and their values
56/// are visually equal as defined by `AttributeValue::eq`.
57impl PartialEq for AttributeEntry {
58    /// Compares two attribute entries for visual equality.
59    ///
60    /// # Arguments
61    ///
62    /// - `&Self` - The first attribute entry.
63    /// - `&Self` - The second attribute entry.
64    ///
65    /// # Returns
66    ///
67    /// - `bool` - `true` if both names and values match.
68    fn eq(&self, other: &Self) -> bool {
69        self.get_name() == other.get_name() && self.get_value() == other.get_value()
70    }
71}
72
73/// Visual equality comparison for CSS classes.
74///
75/// Two CSS classes are considered equal when their class names match,
76/// since the name uniquely identifies the visual style rule.
77impl PartialEq for CssClass {
78    /// Compares two CSS classes by name.
79    ///
80    /// # Arguments
81    ///
82    /// - `&Self` - The first CSS class.
83    /// - `&Self` - The second CSS class.
84    ///
85    /// # Returns
86    ///
87    /// - `bool` - `true` if the class names match.
88    fn eq(&self, other: &Self) -> bool {
89        self.get_name() == other.get_name()
90    }
91}
92
93/// Implementation of style CSS serialization.
94impl Style {
95    /// Adds a style property.
96    ///
97    /// Property names are automatically converted from snake_case to kebab-case
98    /// (e.g., `flex_direction` becomes `flex-direction`).
99    ///
100    /// # Arguments
101    ///
102    /// - `N` - The property name (snake_case will be converted to kebab-case).
103    /// - `V` - The property value.
104    ///
105    /// # Returns
106    ///
107    /// - `Self` - This style with the property added.
108    pub fn property<N, V>(mut self, name: N, value: V) -> Self
109    where
110        N: AsRef<str>,
111        V: AsRef<str>,
112    {
113        self.get_mut_properties().push(StyleProperty::new(
114            name.as_ref().replace('_', "-"),
115            value.as_ref().to_string(),
116        ));
117        self
118    }
119
120    /// Converts the style to a CSS string.
121    ///
122    /// # Returns
123    ///
124    /// - `String` - The CSS string representation.
125    pub fn to_css_string(&self) -> String {
126        self.get_properties()
127            .iter()
128            .map(|style: &StyleProperty| format!("{}: {};", style.get_name(), style.get_value()))
129            .collect::<Vec<String>>()
130            .join(" ")
131    }
132
133    /// Builds a CSS style string from an array of key-value pairs.
134    ///
135    /// This function is used by the `html!` macro to convert static `style:`
136    /// attributes into a CSS string without allocating intermediate `Style`
137    /// and `Vec<StyleProperty>` objects. Keys are converted from snake_case
138    /// to kebab-case automatically.
139    ///
140    /// # Arguments
141    ///
142    /// - `&[(&str, &str)]` - An array of CSS property name-value pairs.
143    ///
144    /// # Returns
145    ///
146    /// - `String` - The CSS string (e.g., `"margin: 0 auto; max-width: 800px;"`).
147    pub fn create_style_string(props: &[(&str, &str)]) -> String {
148        let mut result: String = String::new();
149        for (key, value) in props {
150            if !result.is_empty() {
151                result.push(' ');
152            }
153            result.push_str(&key.replace('_', "-"));
154            result.push_str(": ");
155            result.push_str(value);
156            result.push(';');
157        }
158        result
159    }
160}
161
162/// Provides a default empty style.
163impl Default for Style {
164    /// Returns a default `Style` with no properties.
165    ///
166    /// # Returns
167    ///
168    /// - `Self` - An empty style.
169    fn default() -> Self {
170        Self::new(Vec::new())
171    }
172}
173
174/// Implementation of CssClass construction and style injection.
175impl CssClass {
176    /// Creates a new CSS class with the given name and style declarations.
177    ///
178    /// Automatically injects the styles into the DOM upon creation.
179    ///
180    /// # Arguments
181    ///
182    /// - `String` - The class name.
183    /// - `String` - The CSS style declarations.
184    ///
185    /// # Returns
186    ///
187    /// - `Self` - A new CSS class with injected styles.
188    pub fn new(name: String, style: String) -> Self {
189        let mut css_class: CssClass = CssClass::default();
190        css_class.set_name(name);
191        css_class.set_style(style);
192        css_class.inject_style();
193        css_class
194    }
195
196    /// Creates a new CSS class with the given name, style declarations, and pseudo rules.
197    ///
198    /// Automatically injects the base styles, pseudo-class/pseudo-element rules,
199    /// and media query rules into the DOM upon creation.
200    ///
201    /// # Arguments
202    ///
203    /// - `String` - The class name.
204    /// - `String` - The CSS style declarations.
205    /// - `Vec<PseudoRule>` - The pseudo-class and pseudo-element rules.
206    /// - `Vec<MediaRule>` - The media query rules.
207    ///
208    /// # Returns
209    ///
210    /// - `Self` - A new CSS class with injected styles and pseudo rules.
211    pub fn new_with_rules(
212        name: String,
213        style: String,
214        pseudo_rules: Vec<PseudoRule>,
215        media_rules: Vec<MediaRule>,
216    ) -> Self {
217        let mut css_class: CssClass = CssClass::default();
218        css_class.set_name(name);
219        css_class.set_style(style);
220        css_class.set_pseudo_rules(pseudo_rules);
221        css_class.set_media_rules(media_rules);
222        css_class.inject_style();
223        css_class
224    }
225
226    /// Parses pseudo-class/pseudo-element rules from a compact serialization string.
227    ///
228    /// The serialization format is: `:selector { key: value; key: value; }:another { ... }`
229    /// This is used by the `class!` macro for fully static class definitions
230    /// where pseudo rules can be computed at compile time.
231    ///
232    /// # Arguments
233    ///
234    /// - `&str` - The serialized pseudo rules string.
235    ///
236    /// # Returns
237    ///
238    /// - `Vec<PseudoRule>` - The parsed pseudo rules.
239    pub fn parse_pseudo_rules(input: &str) -> Vec<PseudoRule> {
240        let mut rules: Vec<PseudoRule> = Vec::new();
241        let mut remaining: &str = input;
242        while !remaining.is_empty() {
243            let selector_end: Option<usize> = remaining.find(" { ");
244            let Some(sel_end) = selector_end else {
245                break;
246            };
247            let selector: &str = &remaining[..sel_end];
248            let after_selector: &str = remaining[sel_end..].strip_prefix(" { ").unwrap_or("");
249            let style_end: Option<usize> = after_selector.find('}');
250            let Some(st_end) = style_end else {
251                break;
252            };
253            let style: &str = &after_selector[..st_end];
254            if !selector.is_empty() && !style.is_empty() {
255                rules.push(PseudoRule::new(selector.to_string(), style.to_string()));
256            }
257            remaining = after_selector[st_end..].strip_prefix('}').unwrap_or("");
258        }
259        rules
260    }
261
262    /// Parses media query rules from a compact serialization string.
263    ///
264    /// The serialization format is: `@media query { key: value; key: value; }@media query2 { ... }`
265    /// This is used by the `class!` macro for fully static class definitions
266    /// where media rules can be computed at compile time.
267    ///
268    /// # Arguments
269    ///
270    /// - `&str` - The serialized media rules string.
271    ///
272    /// # Returns
273    ///
274    /// - `Vec<MediaRule>` - The parsed media rules.
275    pub fn parse_media_rules(input: &str) -> Vec<MediaRule> {
276        let mut rules: Vec<MediaRule> = Vec::new();
277        let mut remaining: &str = input;
278        while !remaining.is_empty() {
279            if !remaining.starts_with("@media ") {
280                break;
281            }
282            let after_prefix: &str = remaining.strip_prefix("@media ").unwrap_or("");
283            let query_end: Option<usize> = after_prefix.find(" { ");
284            let Some(q_end) = query_end else {
285                break;
286            };
287            let query: &str = &after_prefix[..q_end];
288            let after_query: &str = after_prefix[q_end..].strip_prefix(" { ").unwrap_or("");
289            let style_end: Option<usize> = after_query.find('}');
290            let Some(st_end) = style_end else {
291                break;
292            };
293            let style: &str = &after_query[..st_end];
294            if !query.is_empty() && !style.is_empty() {
295                rules.push(MediaRule::new(query.to_string(), style.to_string()));
296            }
297            remaining = after_query[st_end..].strip_prefix('}').unwrap_or("");
298        }
299        rules
300    }
301
302    /// Injects this class's styles into the DOM if not already present.
303    ///
304    /// Uses a global `HashSet` to track injected class names, avoiding the
305    /// expensive `existing_css.contains(css)` full-text search on every call.
306    /// Builds the class rule, pseudo-class rules, and media rules as CSS text,
307    /// then appends them directly to the `<style>` element via
308    /// `append_child` with a new text node — no read-modify-write of the
309    /// entire stylesheet content.
310    ///
311    /// # Panics
312    ///
313    /// Panics if `window()` or `document()` is unavailable on the current platform.
314    pub fn inject_style(&self) {
315        if !Self::mark_injected(self.get_name().clone()) {
316            return;
317        }
318        let class_rule: String = format!(".{} {{ {} }}", self.get_name(), self.get_style());
319        let mut css: String = class_rule;
320        for pseudo_rule in self.get_pseudo_rules() {
321            if !pseudo_rule.get_style().is_empty() {
322                let pseudo_rule_str: String = format!(
323                    ".{}{} {{ {} }}",
324                    self.get_name(),
325                    pseudo_rule.get_selector(),
326                    pseudo_rule.get_style()
327                );
328                css = format!("{}\n{}", css, pseudo_rule_str);
329            }
330        }
331        for media_rule in self.get_media_rules() {
332            if !media_rule.get_query().is_empty() {
333                let media_rule_str: String = format!(
334                    "@media {} {{ .{} {{ {} }} }}",
335                    media_rule.get_query(),
336                    self.get_name(),
337                    media_rule.get_style()
338                );
339                css = format!("{}\n{}", css, media_rule_str);
340            }
341        }
342        Self::append_css(&css);
343    }
344
345    /// Marks a class name as injected in the global `HashSet`.
346    ///
347    /// Returns `false` if the class was already injected (no-op), `true`
348    /// if this is the first injection.
349    ///
350    /// # Arguments
351    ///
352    /// - `String` - The class name to mark as injected.
353    ///
354    /// # Returns
355    ///
356    /// - `bool` - `true` if newly injected, `false` if already present.
357    fn mark_injected(class_name: String) -> bool {
358        mark_injected_class(class_name)
359    }
360
361    /// Appends CSS text directly to the shared `<style>` element.
362    ///
363    /// Creates a new text node and appends it as a child of the `<style>`
364    /// element, avoiding the read-modify-write pattern of reading the entire
365    /// `innerText`, concatenating, and setting it back.
366    ///
367    /// # Arguments
368    ///
369    /// - `&str` - The CSS text to append.
370    ///
371    /// # Panics
372    ///
373    /// Panics if `window()` or `document()` is unavailable on the current platform.
374    fn append_css(css: &str) {
375        let style_id: &str = "euv-css-injected";
376        let document: Document = window()
377            .expect("no global window exists")
378            .document()
379            .expect("no document exists");
380        let style_element: HtmlStyleElement = match document.get_element_by_id(style_id) {
381            Some(el) => el.dyn_into::<HtmlStyleElement>().unwrap(),
382            None => {
383                let el: HtmlStyleElement = document
384                    .create_element("style")
385                    .unwrap()
386                    .dyn_into::<HtmlStyleElement>()
387                    .unwrap();
388                el.set_id(style_id);
389                document.head().unwrap().append_child(&el).unwrap();
390                el
391            }
392        };
393        if !css.is_empty() {
394            let text_node: Text = document.create_text_node(css);
395            style_element.append_child(&text_node).unwrap();
396        }
397    }
398
399    /// Injects CSS text into the shared `<style>` element in the DOM.
400    ///
401    /// Delegates to [`CssClass::append_css`] for the actual DOM append.
402    /// Unlike the previous implementation, this does not read the existing
403    /// stylesheet content or perform a full-text `contains` search.
404    ///
405    /// # Arguments
406    ///
407    /// - `&str` - The CSS text to inject (e.g., reset styles, keyframes, media queries).
408    ///
409    /// # Panics
410    ///
411    /// Panics if `window()` or `document()` is unavailable on the current platform.
412    pub fn inject_css(css: &str) {
413        Self::append_css(css);
414    }
415}
416
417/// Displays the CSS class name.
418///
419/// This enables `format!("{}", css_class)` to produce the class name string,
420/// which is required for reactive `if` conditions in `class:` attributes.
421impl std::fmt::Display for CssClass {
422    /// Formats the CSS class as its name string.
423    ///
424    /// # Arguments
425    ///
426    /// - `&mut Formatter` - The formatter.
427    ///
428    /// # Returns
429    ///
430    /// - `std::fmt::Result` - The formatting result.
431    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432        write!(f, "{}", self.get_name())
433    }
434}